Files
ppsspp/Common/GPU/Vulkan/VulkanContext.cpp
T
Henrik RydgårdandClaude Opus 5 6862bc1721 Vulkan: Fix threading issues around pipeline layouts and the delete list
pipelineLayouts_ was mutated from the main thread (CreatePipelineLayout, and the
deferred callback queued by DestroyPipelineLayout) while the render thread walked
it every frame in FlushDescriptors. Exiting a game in Vulkan mode hits this
reliably: ~GPU_Vulkan stops the render thread and destroys the draw engine's
layout, but the destruction is deferred onto the delete list and doesn't actually
run until a BeginFrame two frames later, with the render thread running again.
Guard the list, and the lifetime of the layouts in it, with a mutex.

The global delete list had the same problem - VulkanDescSetPool::Recreate queues
the old pool from FlushDescSets on the render thread, which happens for real once
a game goes past the initial 1024 descriptors, while the main thread moves the
list into the current frame's list in EndFrame(). Lock the queueing functions and
Take's source list.

While in there:
* Take() didn't move queryPools_, so query pools queued for deletion sat on the
  global list until device teardown instead of being deleted a few frames later.
* PerformDeletes now drains into a local list before destroying anything. A
  callback is allowed to queue further deletes (~VKFramebuffer's does, via
  ~VKRFramebuffer), which used to append to the very vector being iterated.
  They now get the normal deferral instead of running in the same pass.
* Missing semicolon in BeginFrame that only compiles because VLOG is empty.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vd8ntC2brCUtCrDJMqLbs8
2026-08-28 22:43:58 +02:00

2146 lines
82 KiB
C++

#define __STDC_LIMIT_MACROS
#include <cstdlib>
#include <cstdint>
#include <cstring>
#include <iostream>
#include "Common/System/System.h"
#include "Common/System/Display.h"
#include "Common/Log.h"
#include "Common/GPU/Shader.h"
#include "Common/GPU/MiscTypes.h"
#include "Common/GPU/Vulkan/VulkanContext.h"
#include "Common/GPU/Vulkan/VulkanDebug.h"
#include "Common/StringUtils.h"
#ifdef USE_CRT_DBG
#undef new
#endif
#include "ext/vma/vk_mem_alloc.h"
// Change this to 1, 2, and 3 to fake failures in a few places, so that
// we can test our fallback-to-GL code.
#define SIMULATE_VULKAN_FAILURE 0
#include "ext/glslang/SPIRV/GlslangToSpv.h"
#ifdef USE_CRT_DBG
#define new DBG_NEW
#endif
using namespace PPSSPP_VK;
VulkanLogOptions g_LogOptions;
static const char * const validationLayers[] = {
"VK_LAYER_KHRONOS_validation",
/*
// For layers included in the Android NDK.
"VK_LAYER_GOOGLE_threading",
"VK_LAYER_LUNARG_parameter_validation",
"VK_LAYER_LUNARG_core_validation",
"VK_LAYER_LUNARG_image",
"VK_LAYER_LUNARG_object_tracker",
"VK_LAYER_LUNARG_swapchain",
"VK_LAYER_GOOGLE_unique_objects",
*/
};
std::string VulkanVendorString(uint32_t vendorId) {
switch (vendorId) {
case VULKAN_VENDOR_INTEL: return "Intel";
case VULKAN_VENDOR_NVIDIA: return "NVIDIA";
case VULKAN_VENDOR_AMD: return "AMD";
case VULKAN_VENDOR_ARM: return "ARM";
case VULKAN_VENDOR_QUALCOMM: return "Qualcomm";
case VULKAN_VENDOR_IMGTEC: return "Imagination";
case VULKAN_VENDOR_APPLE: return "Apple";
case VULKAN_VENDOR_MESA: return "Mesa";
default:
return StringFromFormat("%08x", vendorId);
}
}
const char *VulkanPresentModeToString(VkPresentModeKHR presentMode) {
switch (presentMode) {
case VK_PRESENT_MODE_IMMEDIATE_KHR: return "IMMEDIATE";
case VK_PRESENT_MODE_MAILBOX_KHR: return "MAILBOX";
case VK_PRESENT_MODE_FIFO_KHR: return "FIFO";
case VK_PRESENT_MODE_FIFO_RELAXED_KHR: return "FIFO_RELAXED";
case VK_PRESENT_MODE_SHARED_DEMAND_REFRESH_KHR: return "SHARED_DEMAND_REFRESH_KHR";
case VK_PRESENT_MODE_SHARED_CONTINUOUS_REFRESH_KHR: return "SHARED_CONTINUOUS_REFRESH_KHR";
case VK_PRESENT_MODE_FIFO_LATEST_READY_KHR: return "FIFO_LATEST_READY";
default: return "UNKNOWN";
}
}
const char *VulkanImageLayoutToString(VkImageLayout imageLayout) {
switch (imageLayout) {
case VK_IMAGE_LAYOUT_UNDEFINED: return "UNDEFINED";
case VK_IMAGE_LAYOUT_GENERAL: return "GENERAL";
case VK_IMAGE_LAYOUT_PREINITIALIZED: return "PREINITIALIZED";
case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: return "TRANSFER_SRC_OPTIMAL";
case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: return "TRANSFER_DST_OPTIMAL";
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: return "SHADER_READ_ONLY_OPTIMAL";
case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: return "COLOR_ATTACHMENT_OPTIMAL";
case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL: return "DEPTH_STENCIL_ATTACHMENT_OPTIMAL";
default: return "OTHER";
}
}
const char *WindowSystemToString(WindowSystem winsys) {
switch (winsys) {
case WINDOWSYSTEM_UNINITIALIZED: return "UNINITIALIZED";
case WINDOWSYSTEM_WIN32: return "WIN32";
case WINDOWSYSTEM_ANDROID: return "ANDROID";
case WINDOWSYSTEM_METAL_EXT: return "METAL_EXT";
case WINDOWSYSTEM_XLIB: return "XLIB";
case WINDOWSYSTEM_XCB: return "XCB";
case WINDOWSYSTEM_WAYLAND: return "WAYLAND";
case WINDOWSYSTEM_DISPLAY: return "DISPLAY";
case WINDOWSYSTEM_SDL: return "SDL";
case WINDOWSYSTEM_NONE: return "NONE";
default:
return "UNKNOWN";
}
}
VulkanContext::VulkanContext() {
// Do nothing here.
}
VkResult VulkanContext::CreateInstance(const CreateInfo &info) {
if (!vkCreateInstance) {
init_error_ = "Vulkan not loaded - can't create instance";
return VK_ERROR_INITIALIZATION_FAILED;
}
if (info.flags & VulkanInitFlags::DISABLE_IMPLICIT_LAYERS) {
// https://github.com/KhronosGroup/Vulkan-Loader/blob/main/docs/LoaderDebugging.md
#if PPSSPP_PLATFORM(WINDOWS)
#if !PPSSPP_PLATFORM(UWP)
// Windows uses _putenv_s
_putenv_s("VK_LOADER_LAYERS_DISABLE", "~implicit~");
#endif
#else
// POSIX: use setenv
setenv("VK_LOADER_LAYERS_DISABLE", "~implicit~", 1); // overwrite = 1
#endif
}
DetectInstanceApiVersion();
instance_layer_names_.clear();
// We can get the list of layers and extensions without an instance so we can use this information
// to enable the extensions we need that are available.
GetInstanceLayerProperties();
GetInstanceLayerExtensionList(nullptr, &instance_extension_properties_);
if (!IsInstanceExtensionAvailable(VK_KHR_SURFACE_EXTENSION_NAME)) {
// Cannot create a Vulkan display without VK_KHR_SURFACE_EXTENSION.
// Technically we could allow this for headless builds, but meh.
init_error_ = "Vulkan not loaded - no surface extension";
return VK_ERROR_INITIALIZATION_FAILED;
}
createInfo_ = info;
// List extensions to try to enable.
instance_extensions_enabled_.push_back(VK_KHR_SURFACE_EXTENSION_NAME);
#ifdef _WIN32
instance_extensions_enabled_.push_back(VK_KHR_WIN32_SURFACE_EXTENSION_NAME);
#elif defined(__ANDROID__)
instance_extensions_enabled_.push_back(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME);
#else
#if defined(VK_USE_PLATFORM_XLIB_KHR)
if (IsInstanceExtensionAvailable(VK_KHR_XLIB_SURFACE_EXTENSION_NAME)) {
instance_extensions_enabled_.push_back(VK_KHR_XLIB_SURFACE_EXTENSION_NAME);
}
#endif
//#if defined(VK_USE_PLATFORM_XCB_KHR)
// instance_extensions_enabled_.push_back(VK_KHR_XCB_SURFACE_EXTENSION_NAME);
//#endif
#if defined(VK_USE_PLATFORM_WAYLAND_KHR)
if (IsInstanceExtensionAvailable(VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME)) {
instance_extensions_enabled_.push_back(VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME);
}
#endif
#if defined(VK_USE_PLATFORM_DISPLAY_KHR)
if (IsInstanceExtensionAvailable(VK_KHR_DISPLAY_EXTENSION_NAME)) {
instance_extensions_enabled_.push_back(VK_KHR_DISPLAY_EXTENSION_NAME);
}
#endif
#if defined(VK_USE_PLATFORM_METAL_EXT)
if (IsInstanceExtensionAvailable(VK_EXT_METAL_SURFACE_EXTENSION_NAME)) {
instance_extensions_enabled_.push_back(VK_EXT_METAL_SURFACE_EXTENSION_NAME);
}
#endif
#endif
if ((createInfo_.flags & VulkanInitFlags::VALIDATE) && info.customDriver.empty()) {
if (IsInstanceExtensionAvailable(VK_EXT_DEBUG_UTILS_EXTENSION_NAME)) {
// Enable the validation layers
for (size_t i = 0; i < ARRAY_SIZE(validationLayers); i++) {
instance_layer_names_.push_back(validationLayers[i]);
}
instance_extensions_enabled_.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
extensionsLookup_.EXT_debug_utils = true;
INFO_LOG(Log::G3D, "Vulkan debug_utils validation enabled.");
} else {
ERROR_LOG(Log::G3D, "Validation layer extension not available - not enabling Vulkan validation.");
createInfo_.flags &= ~VulkanInitFlags::VALIDATE;
}
}
if (IsInstanceExtensionAvailable(VK_KHR_GET_SURFACE_CAPABILITIES_2_EXTENSION_NAME)) {
instance_extensions_enabled_.push_back(VK_KHR_GET_SURFACE_CAPABILITIES_2_EXTENSION_NAME);
extensionsLookup_.KHR_get_surface_capabilities2 = true;
}
// Uncomment to test GPU backend fallback
// abort();
if (EnableInstanceExtension(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME, VK_API_VERSION_1_1)) {
extensionsLookup_.KHR_get_physical_device_properties2 = true;
}
if (EnableInstanceExtension(VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME, 0)) {
extensionsLookup_.EXT_swapchain_colorspace = true;
}
// Validate that all the instance extensions we ask for are actually available.
for (auto ext : instance_extensions_enabled_) {
if (!IsInstanceExtensionAvailable(ext)) {
WARN_LOG(Log::G3D, "WARNING: Does not seem that instance extension '%s' is available. Trying to proceed anyway.", ext);
}
}
VkApplicationInfo app_info{ VK_STRUCTURE_TYPE_APPLICATION_INFO };
app_info.pApplicationName = info.app_name;
app_info.applicationVersion = info.app_ver;
app_info.pEngineName = info.app_name;
// Let's increment this when we make major engine/context changes.
app_info.engineVersion = 2;
app_info.apiVersion = vulkanInstanceApiVersion_;
VkInstanceCreateInfo inst_info{ VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO };
inst_info.flags = 0;
inst_info.pApplicationInfo = &app_info;
inst_info.enabledLayerCount = (uint32_t)instance_layer_names_.size();
inst_info.ppEnabledLayerNames = instance_layer_names_.size() ? instance_layer_names_.data() : nullptr;
inst_info.enabledExtensionCount = (uint32_t)instance_extensions_enabled_.size();
inst_info.ppEnabledExtensionNames = instance_extensions_enabled_.size() ? instance_extensions_enabled_.data() : nullptr;
#if PPSSPP_PLATFORM(IOS_APP_STORE)
inst_info.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR;
#endif
#if SIMULATE_VULKAN_FAILURE == 2
VkResult res = VK_ERROR_INCOMPATIBLE_DRIVER;
#else
VkResult res = vkCreateInstance(&inst_info, nullptr, &instance_);
#endif
if (res != VK_SUCCESS) {
if (res == VK_ERROR_LAYER_NOT_PRESENT) {
WARN_LOG(Log::G3D, "Validation on but instance layer not available - dropping layers");
// Drop the validation layers and try again.
instance_layer_names_.clear();
inst_info.enabledLayerCount = 0;
inst_info.ppEnabledLayerNames = nullptr;
res = vkCreateInstance(&inst_info, nullptr, &instance_);
if (res != VK_SUCCESS)
ERROR_LOG(Log::G3D, "Failed to create instance even without validation: %d", res);
} else {
ERROR_LOG(Log::G3D, "Failed to create instance : %d", res);
}
}
if (res != VK_SUCCESS) {
init_error_ = "Failed to create Vulkan instance";
return res;
}
return FinishInstanceInit();
}
VkResult VulkanContext::CreateInstanceExternal(VkInstance instance) {
instance_ = instance;
ownsInstance_ = false;
DetectInstanceApiVersion();
// We didn't go through the normal extension-selection dance in CreateInstance() (there's nothing to
// enable - the instance already exists), but IsInstanceExtensionAvailable() and the properties2 codepath
// below still want these populated, so gather them the same way CreateInstance() does.
GetInstanceLayerProperties();
GetInstanceLayerExtensionList(nullptr, &instance_extension_properties_);
// vkGetPhysicalDeviceProperties2/Features2 are core as of Vulkan 1.1, so an adopted 1.1+ instance can
// always use them regardless of whether the owning application explicitly enabled the KHR extension.
extensionsLookup_.KHR_get_physical_device_properties2 =
vulkanInstanceApiVersion_ >= VK_API_VERSION_1_1 || IsInstanceExtensionAvailable(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME);
return FinishInstanceInit();
}
void VulkanContext::DetectInstanceApiVersion() {
// Check which Vulkan version we should request.
// Our code is fine with any version from 1.0 to 1.2, we don't know about higher versions.
vulkanInstanceApiVersion_ = VK_API_VERSION_1_0;
if (vkEnumerateInstanceVersion) {
vkEnumerateInstanceVersion(&vulkanInstanceApiVersion_);
vulkanInstanceApiVersion_ &= 0xFFFFF000; // Remove patch version.
vulkanInstanceApiVersion_ = std::min(VK_API_VERSION_1_4, vulkanInstanceApiVersion_);
std::string versionString = FormatAPIVersion(vulkanInstanceApiVersion_);
INFO_LOG(Log::G3D, "Detected Vulkan API version: %s", versionString.c_str());
}
}
VkResult VulkanContext::FinishInstanceInit() {
VulkanLoadInstanceFunctions(instance_, extensionsLookup_, vulkanInstanceApiVersion_);
if (!CheckLayers(instance_layer_properties_, instance_layer_names_)) {
WARN_LOG(Log::G3D, "CheckLayers for instance failed");
// init_error_ = "Failed to validate instance layers";
// return;
}
uint32_t gpu_count = 1;
#if SIMULATE_VULKAN_FAILURE == 3
gpu_count = 0;
#else
VkResult res = vkEnumeratePhysicalDevices(instance_, &gpu_count, nullptr);
#endif
if (gpu_count <= 0) {
ERROR_LOG(Log::G3D, "Vulkan driver found but no supported GPU is available");
init_error_ = "No Vulkan physical devices found";
if (ownsInstance_) {
vkDestroyInstance(instance_, nullptr);
}
instance_ = nullptr;
return VK_ERROR_INITIALIZATION_FAILED;
}
_dbg_assert_(gpu_count > 0);
physical_devices_.resize(gpu_count);
physicalDeviceProperties_.resize(gpu_count);
res = vkEnumeratePhysicalDevices(instance_, &gpu_count, physical_devices_.data());
if (res != VK_SUCCESS) {
init_error_ = "Failed to enumerate physical devices";
if (ownsInstance_) {
vkDestroyInstance(instance_, nullptr);
}
instance_ = nullptr;
return res;
}
if (extensionsLookup_.KHR_get_physical_device_properties2 && vkGetPhysicalDeviceProperties2) {
for (uint32_t i = 0; i < gpu_count; i++) {
// Now, we need to do a special check for vkGetPhysicalDeviceProperties2. Unfortunately, it is not valid to call it
// if the *device* is below 1.1.
VkPhysicalDeviceProperties tempProps{};
vkGetPhysicalDeviceProperties(physical_devices_[i], &tempProps);
if (tempProps.apiVersion < VK_API_VERSION_1_1) {
// This device is too old to support vkGetPhysicalDeviceProperties2, so we will just use the old function.
vkGetPhysicalDeviceProperties(physical_devices_[i], &physicalDeviceProperties_[i].properties);
continue;
}
VkPhysicalDeviceProperties2 props2{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2};
VkPhysicalDevicePushDescriptorPropertiesKHR pushProps{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PUSH_DESCRIPTOR_PROPERTIES_KHR};
VkPhysicalDeviceExternalMemoryHostPropertiesEXT extHostMemProps{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_MEMORY_HOST_PROPERTIES_EXT};
VkPhysicalDeviceDepthStencilResolveProperties depthStencilResolveProps{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DEPTH_STENCIL_RESOLVE_PROPERTIES};
ChainStruct(props2, &pushProps);
ChainStruct(props2, &extHostMemProps);
ChainStruct(props2, &depthStencilResolveProps);
vkGetPhysicalDeviceProperties2(physical_devices_[i], &props2);
// Don't want bad pointers sitting around. Probably not really necessary.
props2.pNext = nullptr;
pushProps.pNext = nullptr;
extHostMemProps.pNext = nullptr;
depthStencilResolveProps.pNext = nullptr;
physicalDeviceProperties_[i].properties = props2.properties;
physicalDeviceProperties_[i].pushDescriptorProperties = pushProps;
physicalDeviceProperties_[i].externalMemoryHostProperties = extHostMemProps;
physicalDeviceProperties_[i].depthStencilResolve = depthStencilResolveProps;
}
} else {
for (uint32_t i = 0; i < gpu_count; i++) {
vkGetPhysicalDeviceProperties(physical_devices_[i], &physicalDeviceProperties_[i].properties);
}
}
// Log the list of devices.
INFO_LOG(Log::G3D, "%d Vulkan devices found:", (int)physicalDeviceProperties_.size());
for (const auto &props : physicalDeviceProperties_) {
INFO_LOG(Log::G3D, "%s (vendor: %08x)", props.properties.deviceName, props.properties.vendorID);
}
if (extensionsLookup_.EXT_debug_utils) {
_assert_(vkCreateDebugUtilsMessengerEXT != nullptr);
InitDebugUtilsCallback();
}
return VK_SUCCESS;
}
VulkanContext::~VulkanContext() {
_dbg_assert_(instance_ == VK_NULL_HANDLE);
}
void VulkanContext::DestroyInstance() {
if (extensionsLookup_.EXT_debug_utils) {
while (utils_callbacks.size() > 0) {
vkDestroyDebugUtilsMessengerEXT(instance_, utils_callbacks.back(), nullptr);
utils_callbacks.pop_back();
}
}
if (ownsInstance_) {
vkDestroyInstance(instance_, nullptr);
}
VulkanFree();
instance_ = VK_NULL_HANDLE;
}
void VulkanContext::BeginFrame(VkCommandBuffer firstCommandBuffer) {
FrameData *frame = &frame_[curFrame_];
// Process pending deletes.
frame->deleteList.PerformDeletes(this, allocator_);
// VK_NULL_HANDLE when profiler is disabled.
if (firstCommandBuffer) {
frame->profiler.BeginFrame(this, firstCommandBuffer);
}
}
void VulkanContext::EndFrame() {
frame_[curFrame_].deleteList.Take(globalDeleteList_);
curFrame_++;
if (curFrame_ >= inflightFrames_) {
curFrame_ = 0;
}
}
void VulkanContext::UpdateInflightFrames(int n) {
_dbg_assert_(n >= 1 && n <= MAX_INFLIGHT_FRAMES);
inflightFrames_ = n;
if (curFrame_ >= inflightFrames_) {
curFrame_ = 0;
}
}
void VulkanContext::WaitUntilQueueIdle() {
// Should almost never be used
LockQueue();
vkQueueWaitIdle(gfx_queue_);
UnlockQueue();
}
bool VulkanContext::MemoryTypeFromProperties(uint32_t typeBits, VkFlags requirements_mask, uint32_t *typeIndex) {
// Search memtypes to find first index with those properties
for (uint32_t i = 0; i < 32; i++) {
if ((typeBits & 1) == 1) {
// Type is available, does it match user properties?
if ((memory_properties_.memoryTypes[i].propertyFlags & requirements_mask) == requirements_mask) {
*typeIndex = i;
return true;
}
}
typeBits >>= 1;
}
// No memory types matched, return failure
return false;
}
void VulkanContext::DestroySwapchain() {
if (swapchain_ != VK_NULL_HANDLE) {
vkDestroySwapchainKHR(device_, swapchain_, nullptr);
swapchain_ = VK_NULL_HANDLE;
}
swapchainInited_ = false;
}
void VulkanContext::DestroySurface() {
if (surface_ != VK_NULL_HANDLE) {
vkDestroySurfaceKHR(instance_, surface_, nullptr);
surface_ = VK_NULL_HANDLE;
// NOTE: We do not reset winSysData1 and 2, it's useful for debugging to compare them.
}
}
VkResult VulkanContext::GetInstanceLayerExtensionList(const char *layerName, std::vector<VkExtensionProperties> *extensions) {
VkResult res;
do {
uint32_t instance_extension_count;
res = vkEnumerateInstanceExtensionProperties(layerName, &instance_extension_count, nullptr);
if (res != VK_SUCCESS)
return res;
if (instance_extension_count == 0)
return VK_SUCCESS;
extensions->resize(instance_extension_count);
res = vkEnumerateInstanceExtensionProperties(layerName, &instance_extension_count, extensions->data());
} while (res == VK_INCOMPLETE);
return res;
}
VkResult VulkanContext::GetInstanceLayerProperties() {
/*
* It's possible, though very rare, that the number of
* instance layers could change. For example, installing something
* could include new layers that the loader would pick up
* between the initial query for the count and the
* request for VkLayerProperties. The loader indicates that
* by returning a VK_INCOMPLETE status and will update the
* the count parameter.
* The count parameter will be updated with the number of
* entries loaded into the data pointer - in case the number
* of layers went down or is smaller than the size given.
*/
uint32_t instance_layer_count;
std::vector<VkLayerProperties> vk_props;
VkResult res;
do {
res = vkEnumerateInstanceLayerProperties(&instance_layer_count, nullptr);
if (res != VK_SUCCESS)
return res;
if (!instance_layer_count)
return VK_SUCCESS;
vk_props.resize(instance_layer_count);
res = vkEnumerateInstanceLayerProperties(&instance_layer_count, vk_props.data());
} while (res == VK_INCOMPLETE);
// Now gather the extension list for each instance layer.
// (TODO: Is this meaningful, or used for anything?)
for (uint32_t i = 0; i < instance_layer_count; i++) {
LayerProperties layer_props;
layer_props.properties = vk_props[i];
res = GetInstanceLayerExtensionList(layer_props.properties.layerName, &layer_props.extensions);
if (res != VK_SUCCESS)
return res;
instance_layer_properties_.push_back(layer_props);
}
return res;
}
VkResult VulkanContext::GetDeviceExtensionList(std::vector<VkExtensionProperties> *extensions) {
VkResult res;
do {
uint32_t device_extension_count;
res = vkEnumerateDeviceExtensionProperties(physical_devices_[physical_device_], nullptr, &device_extension_count, nullptr);
if (res != VK_SUCCESS)
return res;
if (!device_extension_count)
return VK_SUCCESS;
extensions->resize(device_extension_count);
res = vkEnumerateDeviceExtensionProperties(physical_devices_[physical_device_], nullptr, &device_extension_count, extensions->data());
} while (res == VK_INCOMPLETE);
return res;
}
// Returns true if all layer names specified in check_names can be found in given layer properties.
bool VulkanContext::CheckLayers(const std::vector<LayerProperties> &layer_props, const std::vector<const char *> &layer_names) const {
uint32_t check_count = (uint32_t)layer_names.size();
uint32_t layer_count = (uint32_t)layer_props.size();
for (uint32_t i = 0; i < check_count; i++) {
bool found = false;
for (uint32_t j = 0; j < layer_count; j++) {
if (!strcmp(layer_names[i], layer_props[j].properties.layerName)) {
found = true;
}
}
if (!found) {
std::cout << "Cannot find layer: " << layer_names[i] << std::endl;
return false;
}
}
return true;
}
int VulkanContext::GetPhysicalDeviceByName(std::string_view name) const {
for (size_t i = 0; i < physical_devices_.size(); i++) {
if (equals(physicalDeviceProperties_[i].properties.deviceName, name))
return (int)i;
}
return -1;
}
int VulkanContext::GetBestPhysicalDevice() const {
// Rules: Prefer discrete over embedded.
// Prefer nVidia over Intel.
int maxScore = -1;
int best = -1;
for (size_t i = 0; i < physical_devices_.size(); i++) {
int score = 0;
VkPhysicalDeviceProperties props;
vkGetPhysicalDeviceProperties(physical_devices_[i], &props);
switch (props.deviceType) {
case VK_PHYSICAL_DEVICE_TYPE_CPU:
score += 1;
break;
case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU:
score += 2;
break;
case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU:
score += 20;
break;
case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU:
score += 10;
break;
default:
break;
}
if (props.vendorID == VULKAN_VENDOR_AMD) {
score += 5;
} else if (props.vendorID == VULKAN_VENDOR_NVIDIA) {
score += 5;
}
if (score > maxScore) {
best = (int)i;
maxScore = score;
}
}
return best;
}
bool VulkanContext::EnableDeviceExtension(const char *extension, uint32_t coreVersion) {
if (coreVersion != 0 && vulkanDeviceApiVersion_ >= coreVersion) {
return true;
}
for (const char *alreadyEnabled : device_extensions_enabled_) {
if (!strcmp(alreadyEnabled, extension)) {
return true;
}
}
for (auto &iter : device_extension_properties_) {
if (!strcmp(iter.extensionName, extension)) {
device_extensions_enabled_.push_back(extension);
return true;
}
}
return false;
}
bool VulkanContext::EnableInstanceExtension(const char *extension, uint32_t coreVersion) {
if (coreVersion != 0 && vulkanInstanceApiVersion_ >= coreVersion) {
return true;
}
for (auto &iter : instance_extension_properties_) {
if (!strcmp(iter.extensionName, extension)) {
instance_extensions_enabled_.push_back(extension);
return true;
}
}
return false;
}
VkResult VulkanContext::CreateDevice(int physical_device, const std::vector<const char *> &extraDeviceExtensions, const VkPhysicalDeviceFeatures *extraRequiredFeatures) {
physical_device_ = physical_device;
INFO_LOG(Log::G3D, "Chose physical device %d: %s", physical_device, physicalDeviceProperties_[physical_device].properties.deviceName);
vulkanDeviceApiVersion_ = physicalDeviceProperties_[physical_device].properties.apiVersion;
queue_count = 0;
vkGetPhysicalDeviceQueueFamilyProperties(physical_devices_[physical_device_], &queue_count, nullptr);
_dbg_assert_(queue_count >= 1);
queueFamilyProperties_.resize(queue_count);
vkGetPhysicalDeviceQueueFamilyProperties(physical_devices_[physical_device_], &queue_count, queueFamilyProperties_.data());
_dbg_assert_(queue_count >= 1);
// Detect preferred depth/stencil formats, in this order. All supported devices will support at least one of these.
static const VkFormat depthStencilFormats[] = {
VK_FORMAT_D24_UNORM_S8_UINT,
VK_FORMAT_D32_SFLOAT_S8_UINT,
VK_FORMAT_D16_UNORM_S8_UINT,
};
deviceInfo_.preferredDepthStencilFormat = VK_FORMAT_UNDEFINED;
for (size_t i = 0; i < ARRAY_SIZE(depthStencilFormats); i++) {
VkFormatProperties props;
vkGetPhysicalDeviceFormatProperties(physical_devices_[physical_device_], depthStencilFormats[i], &props);
if (props.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) {
deviceInfo_.preferredDepthStencilFormat = depthStencilFormats[i];
break;
}
}
_assert_msg_(deviceInfo_.preferredDepthStencilFormat != VK_FORMAT_UNDEFINED, "Could not find a usable depth stencil format.");
VkFormatProperties preferredProps;
vkGetPhysicalDeviceFormatProperties(physical_devices_[physical_device_], deviceInfo_.preferredDepthStencilFormat, &preferredProps);
if ((preferredProps.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT) &&
(preferredProps.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT)) {
deviceInfo_.canBlitToPreferredDepthStencilFormat = true;
}
// This is as good a place as any to do this. Though, we don't use this much anymore after we added
// support for VMA.
vkGetPhysicalDeviceMemoryProperties(physical_devices_[physical_device_], &memory_properties_);
DEBUG_LOG(Log::G3D, "Memory Types (%d):", memory_properties_.memoryTypeCount);
for (int i = 0; i < (int)memory_properties_.memoryTypeCount; i++) {
// Don't bother printing dummy memory types.
if (!memory_properties_.memoryTypes[i].propertyFlags)
continue;
DEBUG_LOG(Log::G3D, " %d: Heap %d; Flags: %s%s%s%s ", i, memory_properties_.memoryTypes[i].heapIndex,
(memory_properties_.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) ? "DEVICE_LOCAL " : "",
(memory_properties_.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) ? "HOST_VISIBLE " : "",
(memory_properties_.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_HOST_CACHED_BIT) ? "HOST_CACHED " : "",
(memory_properties_.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) ? "HOST_COHERENT " : "");
}
GetDeviceExtensionList(&device_extension_properties_);
device_extensions_enabled_.push_back(VK_KHR_SWAPCHAIN_EXTENSION_NAME);
// A host application (e.g. a libretro frontend) may require extra device extensions on top of what we'd
// normally ask for.
for (const char *extraExtension : extraDeviceExtensions) {
EnableDeviceExtension(extraExtension, 0);
}
if (!init_error_.empty() || physical_device_ < 0) {
ERROR_LOG(Log::G3D, "Vulkan init failed: %s", init_error_.c_str());
return VK_ERROR_INITIALIZATION_FAILED;
}
VkDeviceQueueCreateInfo queue_info{ VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO };
float queue_priorities[1] = { 1.0f };
queue_info.queueCount = 1;
queue_info.pQueuePriorities = queue_priorities;
bool found = false;
for (int i = 0; i < (int)queue_count; i++) {
if (queueFamilyProperties_[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
queue_info.queueFamilyIndex = i;
found = true;
break;
}
}
_dbg_assert_(found);
// TODO: A lot of these are on by default in later Vulkan versions, should check for that, technically.
extensionsLookup_.KHR_maintenance1 = EnableDeviceExtension(VK_KHR_MAINTENANCE1_EXTENSION_NAME, VK_API_VERSION_1_1);
extensionsLookup_.KHR_maintenance2 = EnableDeviceExtension(VK_KHR_MAINTENANCE2_EXTENSION_NAME, VK_API_VERSION_1_1);
extensionsLookup_.KHR_maintenance3 = EnableDeviceExtension(VK_KHR_MAINTENANCE3_EXTENSION_NAME, VK_API_VERSION_1_1);
extensionsLookup_.KHR_maintenance4 = EnableDeviceExtension("VK_KHR_maintenance4", VK_API_VERSION_1_3);
extensionsLookup_.KHR_multiview = EnableDeviceExtension(VK_KHR_MULTIVIEW_EXTENSION_NAME, VK_API_VERSION_1_1);
extensionsLookup_.EXT_scalar_block_layout = EnableDeviceExtension(VK_EXT_SCALAR_BLOCK_LAYOUT_EXTENSION_NAME, VK_API_VERSION_1_2);
if (EnableDeviceExtension(VK_KHR_GET_MEMORY_REQUIREMENTS_2_EXTENSION_NAME, VK_API_VERSION_1_1)) {
extensionsLookup_.KHR_get_memory_requirements2 = true;
extensionsLookup_.KHR_dedicated_allocation = EnableDeviceExtension(VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME, VK_API_VERSION_1_1);
}
if (EnableDeviceExtension(VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME, VK_API_VERSION_1_2)) {
extensionsLookup_.KHR_create_renderpass2 = true;
extensionsLookup_.KHR_depth_stencil_resolve = EnableDeviceExtension(VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME, VK_API_VERSION_1_2);
}
extensionsLookup_.EXT_shader_stencil_export = EnableDeviceExtension(VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME, 0);
extensionsLookup_.EXT_fragment_shader_interlock = EnableDeviceExtension(VK_EXT_FRAGMENT_SHADER_INTERLOCK_EXTENSION_NAME, 0);
extensionsLookup_.ARM_rasterization_order_attachment_access = EnableDeviceExtension(VK_ARM_RASTERIZATION_ORDER_ATTACHMENT_ACCESS_EXTENSION_NAME, 0);
#if !PPSSPP_PLATFORM(MAC) && !PPSSPP_PLATFORM(IOS)
extensionsLookup_.GOOGLE_display_timing = EnableDeviceExtension(VK_GOOGLE_DISPLAY_TIMING_EXTENSION_NAME, 0);
#endif
if (!extensionsLookup_.GOOGLE_display_timing) {
extensionsLookup_.KHR_present_id = EnableDeviceExtension(VK_KHR_PRESENT_ID_EXTENSION_NAME, 0);
extensionsLookup_.KHR_present_wait = EnableDeviceExtension(VK_KHR_PRESENT_WAIT_EXTENSION_NAME, 0);
}
extensionsLookup_.EXT_provoking_vertex = EnableDeviceExtension(VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME, 0);
if (extensionsLookup_.KHR_get_surface_capabilities2) {
#ifdef VK_EXT_full_screen_exclusive
extensionsLookup_.EXT_full_screen_exclusive = EnableDeviceExtension(VK_EXT_FULL_SCREEN_EXCLUSIVE_EXTENSION_NAME, 0);
#endif
}
extensionsLookup_.KHR_present_mode_fifo_latest_ready = EnableDeviceExtension(VK_KHR_PRESENT_MODE_FIFO_LATEST_READY_EXTENSION_NAME, 0);
if (!extensionsLookup_.KHR_present_mode_fifo_latest_ready) {
// Enable the EXT extension instead if available, it's equivalent (was promoted).
extensionsLookup_.KHR_present_mode_fifo_latest_ready = EnableDeviceExtension(VK_EXT_PRESENT_MODE_FIFO_LATEST_READY_EXTENSION_NAME, 0);
}
// Optional features
if (extensionsLookup_.KHR_get_physical_device_properties2 && vkGetPhysicalDeviceFeatures2) {
VkPhysicalDeviceFeatures2 features2{ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2_KHR };
// Add to chain even if not supported, GetPhysicalDeviceFeatures is supposed to ignore unknown structs.
VkPhysicalDeviceMultiviewFeatures multiViewFeatures{ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MULTIVIEW_FEATURES };
VkPhysicalDevicePresentWaitFeaturesKHR presentWaitFeatures{ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_WAIT_FEATURES_KHR };
VkPhysicalDevicePresentIdFeaturesKHR presentIdFeatures{ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_ID_FEATURES_KHR };
VkPhysicalDeviceProvokingVertexFeaturesEXT provokingVertexFeatures{ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROVOKING_VERTEX_FEATURES_EXT };
VkPhysicalDevicePresentModeFifoLatestReadyFeaturesKHR presentModeFifoProps{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_MODE_FIFO_LATEST_READY_FEATURES_KHR};
VkPhysicalDeviceScalarBlockLayoutFeatures scalarBlockLayoutFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SCALAR_BLOCK_LAYOUT_FEATURES};
ChainStruct(features2, &multiViewFeatures);
if (extensionsLookup_.KHR_present_wait) {
ChainStruct(features2, &presentWaitFeatures);
}
if (extensionsLookup_.KHR_present_id) {
ChainStruct(features2, &presentIdFeatures);
}
if (extensionsLookup_.EXT_provoking_vertex) {
ChainStruct(features2, &provokingVertexFeatures);
}
if (extensionsLookup_.KHR_present_mode_fifo_latest_ready) {
ChainStruct(features2, &presentModeFifoProps);
}
if (extensionsLookup_.EXT_scalar_block_layout) {
ChainStruct(features2, &scalarBlockLayoutFeatures);
}
vkGetPhysicalDeviceFeatures2(physical_devices_[physical_device_], &features2);
deviceFeatures_.available.standard = features2.features;
deviceFeatures_.available.multiview = multiViewFeatures;
if (extensionsLookup_.EXT_scalar_block_layout) {
deviceFeatures_.available.scalarBlockLayout = scalarBlockLayoutFeatures;
}
if (extensionsLookup_.KHR_present_wait) {
deviceFeatures_.available.presentWait = presentWaitFeatures;
}
if (extensionsLookup_.KHR_present_id) {
deviceFeatures_.available.presentId = presentIdFeatures;
}
if (extensionsLookup_.EXT_provoking_vertex) {
deviceFeatures_.available.provokingVertex = provokingVertexFeatures;
}
if (extensionsLookup_.KHR_present_mode_fifo_latest_ready) {
deviceFeatures_.available.presentModeFifoProps = presentModeFifoProps;
}
} else {
vkGetPhysicalDeviceFeatures(physical_devices_[physical_device_], &deviceFeatures_.available.standard);
deviceFeatures_.available.multiview = {};
}
deviceFeatures_.enabled = {};
// Enable a few safe ones if they are available.
deviceFeatures_.enabled.standard.dualSrcBlend = deviceFeatures_.available.standard.dualSrcBlend;
deviceFeatures_.enabled.standard.logicOp = deviceFeatures_.available.standard.logicOp;
deviceFeatures_.enabled.standard.depthClamp = deviceFeatures_.available.standard.depthClamp;
deviceFeatures_.enabled.standard.depthBounds = deviceFeatures_.available.standard.depthBounds;
deviceFeatures_.enabled.standard.samplerAnisotropy = deviceFeatures_.available.standard.samplerAnisotropy;
deviceFeatures_.enabled.standard.shaderClipDistance = deviceFeatures_.available.standard.shaderClipDistance;
deviceFeatures_.enabled.standard.shaderCullDistance = deviceFeatures_.available.standard.shaderCullDistance;
deviceFeatures_.enabled.standard.geometryShader = deviceFeatures_.available.standard.geometryShader;
deviceFeatures_.enabled.standard.sampleRateShading = deviceFeatures_.available.standard.sampleRateShading;
// A host application (e.g. a libretro frontend) may require some additional features to be enabled.
if (extraRequiredFeatures) {
VkBool32 *enabled = (VkBool32 *)&deviceFeatures_.enabled.standard;
const VkBool32 *required = (const VkBool32 *)extraRequiredFeatures;
for (size_t i = 0; i < sizeof(VkPhysicalDeviceFeatures) / sizeof(VkBool32); i++) {
if (required[i]) {
enabled[i] = VK_TRUE;
}
}
}
#ifdef _DEBUG
// For debugging! Although, it might hide problems, so turning it off. Can be useful to rule out classes of issues.
// deviceFeatures_.enabled.standard.robustBufferAccess = deviceFeatures_.available.standard.robustBufferAccess;
#endif
deviceFeatures_.enabled.multiview = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MULTIVIEW_FEATURES };
if (extensionsLookup_.KHR_multiview) {
deviceFeatures_.enabled.multiview.multiview = deviceFeatures_.available.multiview.multiview;
}
// Strangely, on Intel, it reports these as available even though the extension isn't in the list.
deviceFeatures_.enabled.presentId = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_ID_FEATURES_KHR };
if (extensionsLookup_.KHR_present_id) {
deviceFeatures_.enabled.presentId.presentId = deviceFeatures_.available.presentId.presentId;
}
deviceFeatures_.enabled.presentWait = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_WAIT_FEATURES_KHR };
if (extensionsLookup_.KHR_present_wait) {
deviceFeatures_.enabled.presentWait.presentWait = deviceFeatures_.available.presentWait.presentWait;
}
deviceFeatures_.enabled.provokingVertex = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROVOKING_VERTEX_FEATURES_EXT };
if (extensionsLookup_.EXT_provoking_vertex) {
deviceFeatures_.enabled.provokingVertex.provokingVertexLast = true;
}
deviceFeatures_.enabled.presentModeFifoProps = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_MODE_FIFO_LATEST_READY_FEATURES_KHR};
if (extensionsLookup_.KHR_present_mode_fifo_latest_ready) {
deviceFeatures_.enabled.presentModeFifoProps.presentModeFifoLatestReady = deviceFeatures_.available.presentModeFifoProps.presentModeFifoLatestReady;
}
deviceFeatures_.enabled.scalarBlockLayout = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SCALAR_BLOCK_LAYOUT_FEATURES};
if (extensionsLookup_.EXT_scalar_block_layout) {
deviceFeatures_.enabled.scalarBlockLayout.scalarBlockLayout = deviceFeatures_.available.scalarBlockLayout.scalarBlockLayout;
}
// deviceFeatures_.enabled.multiview.multiviewGeometryShader = deviceFeatures_.available.multiview.multiviewGeometryShader;
VkPhysicalDeviceFeatures2 features2{ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2 };
VkDeviceCreateInfo device_info{ VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO };
device_info.queueCreateInfoCount = 1;
device_info.pQueueCreateInfos = &queue_info;
device_info.enabledLayerCount = 0;
device_info.ppEnabledLayerNames = nullptr;
device_info.enabledExtensionCount = (uint32_t)device_extensions_enabled_.size();
device_info.ppEnabledExtensionNames = device_info.enabledExtensionCount ? device_extensions_enabled_.data() : nullptr;
if (extensionsLookup_.KHR_get_physical_device_properties2) {
device_info.pNext = &features2;
features2.features = deviceFeatures_.enabled.standard;
ChainStruct(features2, &deviceFeatures_.enabled.multiview);
if (extensionsLookup_.KHR_present_wait) {
ChainStruct(features2, &deviceFeatures_.enabled.presentWait);
}
if (extensionsLookup_.KHR_present_id) {
ChainStruct(features2, &deviceFeatures_.enabled.presentId);
}
if (extensionsLookup_.EXT_provoking_vertex) {
ChainStruct(features2, &deviceFeatures_.enabled.provokingVertex);
}
if (extensionsLookup_.EXT_scalar_block_layout) {
ChainStruct(features2, &deviceFeatures_.enabled.scalarBlockLayout);
}
if (extensionsLookup_.KHR_present_mode_fifo_latest_ready) {
ChainStruct(features2, &deviceFeatures_.enabled.presentModeFifoProps);
}
} else {
device_info.pEnabledFeatures = &deviceFeatures_.enabled.standard;
}
VkResult res = vkCreateDevice(physical_devices_[physical_device_], &device_info, nullptr, &device_);
if (res != VK_SUCCESS) {
init_error_ = "Unable to create Vulkan device";
ERROR_LOG(Log::G3D, "%s", init_error_.c_str());
} else {
VulkanLoadDeviceFunctions(device_, extensionsLookup_, vulkanDeviceApiVersion_);
}
INFO_LOG(Log::G3D, "Vulkan Device created: %s", physicalDeviceProperties_[physical_device_].properties.deviceName);
// Since we successfully created a device (however we got here, might be interesting in debug), we force the choice to be visible in the menu.
VulkanSetAvailable(true);
VmaAllocatorCreateInfo allocatorInfo = {};
allocatorInfo.vulkanApiVersion = std::min(vulkanDeviceApiVersion_, vulkanInstanceApiVersion_);
allocatorInfo.physicalDevice = physical_devices_[physical_device_];
allocatorInfo.device = device_;
allocatorInfo.instance = instance_;
VkResult result = vmaCreateAllocator(&allocatorInfo, &allocator_);
_assert_(result == VK_SUCCESS);
_assert_(allocator_ != VK_NULL_HANDLE);
// Examine the physical device to figure out super rough performance grade.
// Basically all we want to do is to identify low performance mobile devices
// so we can make decisions on things like texture scaling strategy.
auto &props = physicalDeviceProperties_[physical_device_].properties;
switch (props.vendorID) {
case VULKAN_VENDOR_AMD:
case VULKAN_VENDOR_NVIDIA:
case VULKAN_VENDOR_INTEL:
devicePerfClass_ = PerfClass::FAST;
break;
case VULKAN_VENDOR_ARM:
devicePerfClass_ = PerfClass::SLOW;
{
// Parse the device name as an ultra rough heuristic.
int maliG = 0;
if (sscanf(props.deviceName, "Mali-G%d", &maliG) == 1) {
if (maliG >= 72) {
devicePerfClass_ = PerfClass::FAST;
}
}
}
break;
case VULKAN_VENDOR_QUALCOMM:
devicePerfClass_ = PerfClass::SLOW;
#if PPSSPP_PLATFORM(ANDROID)
// The roughest heuristic ever, this needs improvement.
if (System_GetPropertyInt(SYSPROP_SYSTEMVERSION) >= 30) {
devicePerfClass_ = PerfClass::FAST;
}
#elif PPSSPP_PLATFORM(WINDOWS)
// All the modern Qualcomm PC laptops are fast enough to be called FAST.
devicePerfClass_ = PerfClass::FAST;
#endif
break;
case VULKAN_VENDOR_IMGTEC:
default:
devicePerfClass_ = PerfClass::SLOW;
break;
}
return res;
}
VkResult VulkanContext::InitDebugUtilsCallback() {
VkDebugUtilsMessengerCreateInfoEXT callback1{VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT};
// We're intentionally skipping VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT and
// VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT, just too spammy.
callback1.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT;
callback1.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
callback1.pfnUserCallback = &VulkanDebugUtilsCallback;
callback1.pUserData = (void *)&g_LogOptions;
VkDebugUtilsMessengerEXT messenger;
VkResult res = vkCreateDebugUtilsMessengerEXT(instance_, &callback1, nullptr, &messenger);
if (res != VK_SUCCESS) {
ERROR_LOG(Log::G3D, "Failed to register debug callback with vkCreateDebugUtilsMessengerEXT");
// Do error handling for VK_ERROR_OUT_OF_MEMORY
} else {
INFO_LOG(Log::G3D, "Debug callback registered with vkCreateDebugUtilsMessengerEXT.");
utils_callbacks.push_back(messenger);
}
return res;
}
bool VulkanContext::CreateInstanceAndDevice(const CreateInfo &info, std::string *deviceName) {
VkResult res = CreateInstance(info);
if (res != VK_SUCCESS) {
ERROR_LOG(Log::G3D, "Failed to create vulkan context: %s", InitError().c_str());
VulkanSetAvailable(false);
return false;
}
// TODO: Take the device name into account.
int physicalDevice = GetBestPhysicalDevice();
if (physicalDevice < 0) {
ERROR_LOG(Log::G3D, "No usable Vulkan device found.");
DestroyInstance();
return false;
}
INFO_LOG(Log::G3D, "Creating Vulkan device (flags: %08x)", (u32)info.flags);
if (CreateDevice(physicalDevice) != VK_SUCCESS) {
INFO_LOG(Log::G3D, "Failed to create vulkan device: %s", InitError().c_str());
DestroyInstance();
return false;
}
return true;
}
void VulkanContext::SetDebugNameImpl(uint64_t handle, VkObjectType type, const char *name) {
VkDebugUtilsObjectNameInfoEXT info{ VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT };
info.pObjectName = name;
info.objectHandle = handle;
info.objectType = type;
vkSetDebugUtilsObjectNameEXT(device_, &info);
}
VkResult VulkanContext::InitSurface(WindowSystem winsys, void *data1, void *data2) {
winsys_ = winsys;
if (winsysData1_ != data1 && winsysData1_ != 0) {
WARN_LOG(Log::G3D, "winsysData1 changed from %p to %p", winsysData1_, data1);
}
if (winsysData2_ != data2 && winsysData2_ != 0) {
WARN_LOG(Log::G3D, "winsysData2 changed from %p to %p", winsysData2_, data2);
}
winsysData1_ = data1;
winsysData2_ = data2;
return ReinitSurface();
}
VkResult VulkanContext::ReinitSurface() {
if (surface_ != VK_NULL_HANDLE) {
INFO_LOG(Log::G3D, "Destroying Vulkan surface (%d, %d)", swapChainExtent_.width, swapChainExtent_.height);
vkDestroySurfaceKHR(instance_, surface_, nullptr);
surface_ = VK_NULL_HANDLE;
}
INFO_LOG(Log::G3D, "Creating Vulkan surface for window (winsys=%s data1=%p data2=%p)", WindowSystemToString(winsys_), winsysData1_, winsysData2_);
VkResult retval = VK_SUCCESS;
switch (winsys_) {
#ifdef _WIN32
case WINDOWSYSTEM_WIN32:
{
VkWin32SurfaceCreateInfoKHR win32{ VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR };
win32.flags = 0;
win32.hinstance = (HINSTANCE)winsysData1_;
win32.hwnd = (HWND)winsysData2_;
retval = vkCreateWin32SurfaceKHR(instance_, &win32, nullptr, &surface_);
break;
}
#endif
#if defined(__ANDROID__)
case WINDOWSYSTEM_ANDROID:
{
ANativeWindow *wnd = (ANativeWindow *)winsysData1_;
VkAndroidSurfaceCreateInfoKHR android{ VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR };
android.flags = 0;
android.window = wnd;
retval = vkCreateAndroidSurfaceKHR(instance_, &android, nullptr, &surface_);
break;
}
#endif
#if defined(VK_USE_PLATFORM_METAL_EXT)
case WINDOWSYSTEM_METAL_EXT:
{
VkMetalSurfaceCreateInfoEXT metal{ VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT };
metal.flags = 0;
metal.pLayer = winsysData1_;
metal.pNext = winsysData2_;
retval = vkCreateMetalSurfaceEXT(instance_, &metal, nullptr, &surface_);
break;
}
#endif
#if defined(VK_USE_PLATFORM_XLIB_KHR)
case WINDOWSYSTEM_XLIB:
{
VkXlibSurfaceCreateInfoKHR xlib{ VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR };
xlib.flags = 0;
xlib.dpy = (Display *)winsysData1_;
xlib.window = (Window)winsysData2_;
retval = vkCreateXlibSurfaceKHR(instance_, &xlib, nullptr, &surface_);
break;
}
#endif
#if defined(VK_USE_PLATFORM_XCB_KHR)
case WINDOWSYSTEM_XCB:
{
VkXCBSurfaceCreateInfoKHR xcb{ VK_STRUCTURE_TYPE_XCB_SURFACE_CREATE_INFO_KHR };
xcb.flags = 0;
xcb.connection = (Connection *)winsysData1_;
xcb.window = (Window)(uintptr_t)winsysData2_;
retval = vkCreateXcbSurfaceKHR(instance_, &xcb, nullptr, &surface_);
break;
}
#endif
#if defined(VK_USE_PLATFORM_WAYLAND_KHR)
case WINDOWSYSTEM_WAYLAND:
{
VkWaylandSurfaceCreateInfoKHR wayland{ VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR };
wayland.flags = 0;
wayland.display = (wl_display *)winsysData1_;
wayland.surface = (wl_surface *)winsysData2_;
retval = vkCreateWaylandSurfaceKHR(instance_, &wayland, nullptr, &surface_);
break;
}
#endif
#if defined(VK_USE_PLATFORM_DISPLAY_KHR)
case WINDOWSYSTEM_DISPLAY:
{
VkDisplaySurfaceCreateInfoKHR display{ VK_STRUCTURE_TYPE_DISPLAY_SURFACE_CREATE_INFO_KHR };
#if !defined(__LIBRETRO__)
/*
And when not to use libretro need VkDisplaySurfaceCreateInfoKHR this extension,
then you need to use dlopen to read vulkan loader in VulkanLoader.cpp.
huangzihan China
*/
if(!vkGetPhysicalDeviceDisplayPropertiesKHR ||
!vkGetPhysicalDeviceDisplayPlanePropertiesKHR ||
!vkGetDisplayModePropertiesKHR ||
!vkGetDisplayPlaneSupportedDisplaysKHR ||
!vkGetDisplayPlaneCapabilitiesKHR ) {
_assert_msg_(false, "DISPLAY Vulkan cannot find any vulkan function symbols.");
return VK_ERROR_INITIALIZATION_FAILED;
}
//The following code is for reference:
// https://github.com/vanfanel/ppsspp
// When using the VK_KHR_display extension and not using LIBRETRO, a complete
// VkDisplaySurfaceCreateInfoKHR is needed.
uint32_t display_count;
uint32_t plane_count;
VkDisplayPropertiesKHR *display_props = NULL;
VkDisplayPlanePropertiesKHR *plane_props = NULL;
VkDisplayModePropertiesKHR* mode_props = NULL;
VkExtent2D image_size;
// This is the chosen physical_device, it has been chosen elsewhere.
VkPhysicalDevice phys_device = physical_devices_[physical_device_];
VkDisplayModeKHR display_mode = VK_NULL_HANDLE;
VkDisplayPlaneAlphaFlagBitsKHR alpha_mode = VK_DISPLAY_PLANE_ALPHA_OPAQUE_BIT_KHR;
uint32_t plane = UINT32_MAX;
// For now, use the first available (connected) display.
int display_index = 0;
VkResult result;
bool ret = false;
bool mode_found = false;
int i, j;
// 1 physical device can have N displays connected.
// Vulkan only counts the connected displays.
// Get a list of displays on the physical device.
display_count = 0;
vkGetPhysicalDeviceDisplayPropertiesKHR(phys_device, &display_count, NULL);
if (display_count == 0) {
_assert_msg_(false, "DISPLAY Vulkan couldn't find any displays.");
return VK_ERROR_INITIALIZATION_FAILED;
}
display_props = new VkDisplayPropertiesKHR[display_count];
vkGetPhysicalDeviceDisplayPropertiesKHR(phys_device, &display_count, display_props);
// Get a list of display planes on the physical device.
plane_count = 0;
vkGetPhysicalDeviceDisplayPlanePropertiesKHR(phys_device, &plane_count, NULL);
if (plane_count == 0) {
_assert_msg_(false, "DISPLAY Vulkan couldn't find any planes on the physical device");
return VK_ERROR_INITIALIZATION_FAILED;
}
plane_props = new VkDisplayPlanePropertiesKHR[plane_count];
vkGetPhysicalDeviceDisplayPlanePropertiesKHR(phys_device, &plane_count, plane_props);
// Get the Vulkan display we are going to use.
VkDisplayKHR myDisplay = display_props[display_index].display;
// Get the list of display modes of the display
uint32_t mode_count = 0;
vkGetDisplayModePropertiesKHR(phys_device, myDisplay, &mode_count, NULL);
if (mode_count == 0) {
_assert_msg_(false, "DISPLAY Vulkan couldn't find any video modes on the display");
return VK_ERROR_INITIALIZATION_FAILED;
}
mode_props = new VkDisplayModePropertiesKHR[mode_count];
vkGetDisplayModePropertiesKHR(phys_device, myDisplay, &mode_count, mode_props);
// See if there's an appropiate mode available on the display
display_mode = VK_NULL_HANDLE;
for (i = 0; i < mode_count; ++i)
{
const VkDisplayModePropertiesKHR* mode = &mode_props[i];
if (mode->parameters.visibleRegion.width == g_display.pixel_xres &&
mode->parameters.visibleRegion.height == g_display.pixel_yres)
{
display_mode = mode->displayMode;
mode_found = true;
break;
}
}
// Free the mode list now.
delete [] mode_props;
// If there are no useable modes found on the display, error out
if (display_mode == VK_NULL_HANDLE)
{
_assert_msg_(false, "DISPLAY Vulkan couldn't find any video modes on the display");
return VK_ERROR_INITIALIZATION_FAILED;
}
/* Iterate on the list of planes of the physical device
to find a plane that matches these criteria:
-It must be compatible with the chosen display + mode.
-It isn't currently bound to another display.
-It supports per-pixel alpha, if possible. */
for (i = 0; i < plane_count; i++) {
uint32_t supported_displays_count = 0;
VkDisplayKHR* supported_displays;
VkDisplayPlaneCapabilitiesKHR plane_caps;
/* See if the plane is compatible with the current display. */
vkGetDisplayPlaneSupportedDisplaysKHR(phys_device, i, &supported_displays_count, NULL);
if (supported_displays_count == 0) {
/* This plane doesn't support any displays. Continue to the next plane. */
continue;
}
/* Get the list of displays supported by this plane. */
supported_displays = new VkDisplayKHR[supported_displays_count];
vkGetDisplayPlaneSupportedDisplaysKHR(phys_device, i,
&supported_displays_count, supported_displays);
/* The plane must be bound to the chosen display, or not in use.
If none of these is true, iterate to another plane. */
if ( !( (plane_props[i].currentDisplay == myDisplay) ||
(plane_props[i].currentDisplay == VK_NULL_HANDLE)))
continue;
/* Iterate the list of displays supported by this plane
in order to find out if the chosen display is among them. */
bool plane_supports_display = false;
for (j = 0; j < supported_displays_count; j++) {
if (supported_displays[j] == myDisplay) {
plane_supports_display = true;
break;
}
}
/* Free the list of displays supported by this plane. */
delete [] supported_displays;
/* If the display is not supported by this plane, iterate to the next plane. */
if (!plane_supports_display)
continue;
/* Want a plane that supports the alpha mode we have chosen. */
vkGetDisplayPlaneCapabilitiesKHR(phys_device, display_mode, i, &plane_caps);
if (plane_caps.supportedAlpha & alpha_mode) {
/* Yep, this plane is alright. */
plane = i;
break;
}
}
/* If we couldn't find an appropiate plane, error out. */
if (plane == UINT32_MAX) {
_assert_msg_(false, "DISPLAY Vulkan couldn't find an appropiate plane");
return VK_ERROR_INITIALIZATION_FAILED;
}
// Finally, create the vulkan surface.
image_size.width = g_display.pixel_xres;
image_size.height = g_display.pixel_yres;
display.displayMode = display_mode;
display.imageExtent = image_size;
display.transform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
display.alphaMode = alpha_mode;
display.globalAlpha = 1.0f;
display.planeIndex = plane;
display.planeStackIndex = plane_props[plane].currentStackIndex;
display.pNext = nullptr;
delete [] display_props;
delete [] plane_props;
#endif
display.flags = 0;
retval = vkCreateDisplayPlaneSurfaceKHR(instance_, &display, nullptr, &surface_);
break;
}
#endif
default:
_assert_msg_(false, "Vulkan support for chosen window system (%s) not implemented", WindowSystemToString(winsys_));
return VK_ERROR_INITIALIZATION_FAILED;
}
if (retval != VK_SUCCESS) {
return retval;
}
if (!ChooseQueue()) {
return VK_ERROR_INITIALIZATION_FAILED;
}
for (int i = 0; i < ARRAY_SIZE(frame_); i++) {
frame_[i].profiler.Init(this);
}
// Query presentation modes. We need to know which ones are available for InitSwapchain().
availablePresentModes_.clear();
uint32_t presentModeCount = 0;
VkResult res = vkGetPhysicalDeviceSurfacePresentModesKHR(physical_devices_[physical_device_], surface_, &presentModeCount, nullptr);
_dbg_assert_(res == VK_SUCCESS);
if (res == VK_SUCCESS) {
availablePresentModes_.resize(presentModeCount);
res = vkGetPhysicalDeviceSurfacePresentModesKHR(physical_devices_[physical_device_], surface_, &presentModeCount, availablePresentModes_.data());
_dbg_assert_(res == VK_SUCCESS);
}
return VK_SUCCESS;
}
bool VulkanContext::ChooseQueue() {
// Iterate over each queue to learn whether it supports presenting:
VkBool32 *supportsPresent = new VkBool32[queue_count];
for (uint32_t i = 0; i < queue_count; i++) {
vkGetPhysicalDeviceSurfaceSupportKHR(physical_devices_[physical_device_], i, surface_, &supportsPresent[i]);
}
// Search for a graphics queue and a present queue in the array of queue
// families, try to find one that supports both
uint32_t graphicsQueueNodeIndex = UINT32_MAX;
uint32_t presentQueueNodeIndex = UINT32_MAX;
for (uint32_t i = 0; i < queue_count; i++) {
if ((queueFamilyProperties_[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) != 0) {
if (graphicsQueueNodeIndex == UINT32_MAX) {
graphicsQueueNodeIndex = i;
}
if (supportsPresent[i] == VK_TRUE) {
graphicsQueueNodeIndex = i;
presentQueueNodeIndex = i;
break;
}
}
}
if (presentQueueNodeIndex == UINT32_MAX) {
// If didn't find a queue that supports both graphics and present, then
// find a separate present queue. NOTE: We don't actually currently support this arrangement!
for (uint32_t i = 0; i < queue_count; ++i) {
if (supportsPresent[i] == VK_TRUE) {
presentQueueNodeIndex = i;
break;
}
}
}
delete[] supportsPresent;
// Generate error if could not find both a graphics and a present queue
if (graphicsQueueNodeIndex == UINT32_MAX || presentQueueNodeIndex == UINT32_MAX) {
ERROR_LOG(Log::G3D, "Could not find a graphics and a present queue");
return false;
}
graphics_queue_family_index_ = graphicsQueueNodeIndex;
// Get the list of VkFormats that are supported:
uint32_t formatCount = 0;
VkResult res = vkGetPhysicalDeviceSurfaceFormatsKHR(physical_devices_[physical_device_], surface_, &formatCount, nullptr);
_assert_msg_(res == VK_SUCCESS, "Failed to get formats for device %d: %d", physical_device_, (int)res);
if (res != VK_SUCCESS) {
return false;
}
surfFormats_.resize(formatCount);
res = vkGetPhysicalDeviceSurfaceFormatsKHR(physical_devices_[physical_device_], surface_, &formatCount, surfFormats_.data());
_dbg_assert_(res == VK_SUCCESS);
if (res != VK_SUCCESS) {
return false;
}
// If the format list includes just one entry of VK_FORMAT_UNDEFINED,
// the surface has no preferred format. Otherwise, at least one
// supported format will be returned.
if (formatCount == 0 || (formatCount == 1 && surfFormats_[0].format == VK_FORMAT_UNDEFINED)) {
INFO_LOG(Log::G3D, "swapchain_format: Falling back to B8G8R8A8_UNORM");
swapchainFormat_ = VK_FORMAT_B8G8R8A8_UNORM;
} else {
swapchainFormat_ = VK_FORMAT_UNDEFINED;
for (uint32_t i = 0; i < formatCount; ++i) {
if (surfFormats_[i].colorSpace != VK_COLORSPACE_SRGB_NONLINEAR_KHR) {
continue;
}
if (surfFormats_[i].format == VK_FORMAT_B8G8R8A8_UNORM || surfFormats_[i].format == VK_FORMAT_R8G8B8A8_UNORM) {
swapchainFormat_ = surfFormats_[i].format;
break;
}
}
if (swapchainFormat_ == VK_FORMAT_UNDEFINED) {
// Okay, take the first one then.
swapchainFormat_ = surfFormats_[0].format;
}
INFO_LOG(Log::G3D, "swapchain_format: %s (%d) (/%d)", VulkanFormatToString(swapchainFormat_), (int)swapchainFormat_, formatCount);
}
vkGetDeviceQueue(device_, graphics_queue_family_index_, 0, &gfx_queue_);
return true;
}
bool VulkanContext::ChooseGraphicsQueueWithoutSurface() {
uint32_t graphicsQueueNodeIndex = UINT32_MAX;
for (uint32_t i = 0; i < queue_count; i++) {
if ((queueFamilyProperties_[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) != 0) {
graphicsQueueNodeIndex = i;
break;
}
}
if (graphicsQueueNodeIndex == UINT32_MAX) {
ERROR_LOG(Log::G3D, "Could not find a graphics queue");
return false;
}
graphics_queue_family_index_ = graphicsQueueNodeIndex;
vkGetDeviceQueue(device_, graphics_queue_family_index_, 0, &gfx_queue_);
return true;
}
int clamp(int x, int a, int b) {
if (x < a)
return a;
if (x > b)
return b;
return x;
}
static std::string surface_transforms_to_string(VkSurfaceTransformFlagsKHR transformFlags) {
std::string str;
if (transformFlags & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) str += "IDENTITY ";
if (transformFlags & VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR) str += "ROTATE_90 ";
if (transformFlags & VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR) str += "ROTATE_180 ";
if (transformFlags & VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR) str += "ROTATE_270 ";
if (transformFlags & VK_SURFACE_TRANSFORM_HORIZONTAL_MIRROR_BIT_KHR) str += "HMIRROR ";
if (transformFlags & VK_SURFACE_TRANSFORM_HORIZONTAL_MIRROR_ROTATE_90_BIT_KHR) str += "HMIRROR_90 ";
if (transformFlags & VK_SURFACE_TRANSFORM_HORIZONTAL_MIRROR_ROTATE_180_BIT_KHR) str += "HMIRROR_180 ";
if (transformFlags & VK_SURFACE_TRANSFORM_HORIZONTAL_MIRROR_ROTATE_270_BIT_KHR) str += "HMIRROR_270 ";
if (transformFlags & VK_SURFACE_TRANSFORM_INHERIT_BIT_KHR) str += "INHERIT ";
return str;
}
bool VulkanContext::InitSwapchain(VkPresentModeKHR desiredPresentMode) {
_assert_(physical_device_ >= 0 && physical_device_ < (int)physical_devices_.size());
if (!surface_) {
ERROR_LOG(Log::G3D, "VK: No surface, can't create swapchain");
return false;
}
if (swapchain_) {
INFO_LOG(Log::G3D, "Swapchain already exists, recreating...");
}
VkResult res = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical_devices_[physical_device_], surface_, &surfCapabilities_);
if (res == VK_ERROR_SURFACE_LOST_KHR) {
// Not much to do.
ERROR_LOG(Log::G3D, "VK: Surface lost in InitSwapchain");
return false;
}
if (surfCapabilities_.maxImageExtent.width == 0 || surfCapabilities_.maxImageExtent.height == 0) {
WARN_LOG(Log::G3D, "Max image extent is 0 - app is probably minimized. Faking having a swapchain.");
swapChainExtent_ = {}; // makes it so querying width/height returns 0.
// We pretend to have a swapchain initialized - though we won't actually render to it.
swapchainInited_ = true;
return true;
}
VkExtent2D currentExtent{ surfCapabilities_.currentExtent };
INFO_LOG(Log::G3D, "surfCapabilities_.current: %dx%d", currentExtent.width, currentExtent.height);
// https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkSurfaceCapabilitiesKHR.html
// currentExtent is the current width and height of the surface, or the special value (0xFFFFFFFF, 0xFFFFFFFF) indicating that the surface size will be determined by the extent of a swapchain targeting the surface.
if (currentExtent.width == 0xFFFFFFFFu || currentExtent.height == 0xFFFFFFFFu
#if PPSSPP_PLATFORM(IOS)
|| currentExtent.width == 0 || currentExtent.height == 0
#endif
) {
ERROR_LOG(Log::G3D, "using cbGetDrawSize_ to resolve surface size");
_dbg_assert_((bool)cbGetDrawSize_);
if (cbGetDrawSize_) {
currentExtent = cbGetDrawSize_();
}
}
swapChainExtent_.width = clamp(currentExtent.width, surfCapabilities_.minImageExtent.width, surfCapabilities_.maxImageExtent.width);
swapChainExtent_.height = clamp(currentExtent.height, surfCapabilities_.minImageExtent.height, surfCapabilities_.maxImageExtent.height);
INFO_LOG(Log::G3D, "surfCapabilities_.current after clamp: %dx%d min: %dx%d max: %dx%d computed: %dx%d cbdraw",
currentExtent.width, currentExtent.height,
surfCapabilities_.minImageExtent.width, surfCapabilities_.minImageExtent.height,
surfCapabilities_.maxImageExtent.width, surfCapabilities_.maxImageExtent.height,
swapChainExtent_.width, swapChainExtent_.height);
// TODO: Find a better way to specify the prioritized present mode while being able
// to fall back in a sensible way.
VkPresentModeKHR swapchainPresentMode = VK_PRESENT_MODE_MAX_ENUM_KHR;
// Kind of silly logic now, but at least it performs a final sanity check of the chosen value.
for (size_t i = 0; i < availablePresentModes_.size(); i++) {
bool match = availablePresentModes_[i] == desiredPresentMode;
// Default to the first present mode from the list.
if (match || swapchainPresentMode == VK_PRESENT_MODE_MAX_ENUM_KHR) {
swapchainPresentMode = availablePresentModes_[i];
}
if (match) {
break;
}
}
// Determine the number of VkImage's to use in the swap chain (we desire to
// own only 1 image at a time, besides the images being displayed and
// queued for display):
uint32_t desiredNumberOfSwapChainImages = surfCapabilities_.minImageCount + 1;
if ((surfCapabilities_.maxImageCount > 0) &&
(desiredNumberOfSwapChainImages > surfCapabilities_.maxImageCount)) {
// Application must settle for fewer images than desired:
desiredNumberOfSwapChainImages = surfCapabilities_.maxImageCount;
}
std::string modes = "";
for (size_t i = 0; i < availablePresentModes_.size(); i++) {
modes += VulkanPresentModeToString(availablePresentModes_[i]);
if (i != availablePresentModes_.size() - 1) {
modes += ", ";
}
}
INFO_LOG(Log::G3D, "Supported present modes: %s. Chosen present mode: %d (%s). numSwapChainImages: %d (max: %d)",
modes.c_str(), swapchainPresentMode, VulkanPresentModeToString(swapchainPresentMode),
desiredNumberOfSwapChainImages, surfCapabilities_.maxImageCount);
// We mostly follow the practices from
// https://arm-software.github.io/vulkan_best_practice_for_mobile_developers/samples/surface_rotation/surface_rotation_tutorial.html
//
VkSurfaceTransformFlagBitsKHR preTransform;
std::string supportedTransforms = surface_transforms_to_string(surfCapabilities_.supportedTransforms);
std::string currentTransform = surface_transforms_to_string(surfCapabilities_.currentTransform);
g_display.rotation = DisplayRotation::ROTATE_0;
g_display.rot_matrix.setIdentity();
uint32_t allowedRotations = VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR | VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR | VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR;
// Hack: Don't allow 270 degrees pretransform (inverse landscape), it creates bizarre issues on some devices (see #15773).
allowedRotations &= ~VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR;
if (surfCapabilities_.currentTransform & (VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR | VK_SURFACE_TRANSFORM_INHERIT_BIT_KHR)) {
preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
} else if (surfCapabilities_.currentTransform & allowedRotations) {
// Normal, sensible rotations. Let's handle it.
preTransform = surfCapabilities_.currentTransform;
g_display.rot_matrix.setIdentity();
switch (surfCapabilities_.currentTransform) {
case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR:
g_display.rotation = DisplayRotation::ROTATE_90;
g_display.rot_matrix.setRotationZ90();
std::swap(swapChainExtent_.width, swapChainExtent_.height);
break;
case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR:
g_display.rotation = DisplayRotation::ROTATE_180;
g_display.rot_matrix.setRotationZ180();
break;
case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR:
g_display.rotation = DisplayRotation::ROTATE_270;
g_display.rot_matrix.setRotationZ270();
std::swap(swapChainExtent_.width, swapChainExtent_.height);
break;
default:
_dbg_assert_(false);
}
} else {
// Let the OS rotate the image (potentially slower on many Android devices)
preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
}
// Only log transforms if relevant.
if (surfCapabilities_.supportedTransforms != VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) {
std::string preTransformStr = surface_transforms_to_string(preTransform);
INFO_LOG(Log::G3D, "Transform supported: %s current: %s chosen: %s", supportedTransforms.c_str(), currentTransform.c_str(), preTransformStr.c_str());
}
if (physicalDeviceProperties_[physical_device_].properties.vendorID == VULKAN_VENDOR_IMGTEC) {
u32 driverVersion = physicalDeviceProperties_[physical_device_].properties.driverVersion;
// Cutoff the hack at driver version 1.386.1368 (0x00582558, see issue #15773).
if (driverVersion < 0x00582558) {
INFO_LOG(Log::G3D, "Applying PowerVR hack (rounding off the width!) driverVersion=%08x", driverVersion);
// Swap chain width hack to avoid issue #11743 (PowerVR driver bug).
// To keep the size consistent even with pretransform, do this after the swap. Should be fine.
// This is fixed in newer PowerVR drivers but I don't know the cutoff.
swapChainExtent_.width &= ~31;
// TODO: Also modify display_xres/display_yres appropriately for scissors to match.
// This will get a bit messy. Ideally we should remove that logic from app-android.cpp
// and move it here, but the OpenGL code still needs it.
} else {
INFO_LOG(Log::G3D, "PowerVR driver version new enough (%08x), not applying swapchain width hack", driverVersion);
}
}
VkSwapchainKHR oldSwapchain = swapchain_;
VkSwapchainCreateInfoKHR swap_chain_info{ VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR };
swap_chain_info.surface = surface_;
swap_chain_info.minImageCount = desiredNumberOfSwapChainImages;
swap_chain_info.imageFormat = swapchainFormat_;
swap_chain_info.imageColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR;
swap_chain_info.imageExtent.width = swapChainExtent_.width;
swap_chain_info.imageExtent.height = swapChainExtent_.height;
swap_chain_info.preTransform = preTransform;
swap_chain_info.imageArrayLayers = 1;
swap_chain_info.presentMode = swapchainPresentMode;
swap_chain_info.oldSwapchain = swapchain_;
swap_chain_info.clipped = true;
swap_chain_info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
presentMode_ = swapchainPresentMode;
// We don't support screenshots on Android if TRANSFER_SRC usage flag is not supported.
if (surfCapabilities_.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_SRC_BIT) {
swap_chain_info.imageUsage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
}
swap_chain_info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
swap_chain_info.queueFamilyIndexCount = 0;
swap_chain_info.pQueueFamilyIndices = NULL;
// OPAQUE is not supported everywhere.
if (surfCapabilities_.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR) {
swap_chain_info.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
} else {
// This should be supported anywhere, and is the only thing supported on the SHIELD TV, for example.
swap_chain_info.compositeAlpha = VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR;
}
#ifdef VK_EXT_full_screen_exclusive
VkSurfaceFullScreenExclusiveInfoEXT fullScreenInfo{ VK_STRUCTURE_TYPE_SURFACE_FULL_SCREEN_EXCLUSIVE_INFO_EXT };
VkSurfaceFullScreenExclusiveWin32InfoEXT win32ExclusiveInfo{ VK_STRUCTURE_TYPE_SURFACE_FULL_SCREEN_EXCLUSIVE_WIN32_INFO_EXT };
if (extensionsLookup_.EXT_full_screen_exclusive) {
fullScreenInfo.fullScreenExclusive = fullScreenExclusiveMode_;
if (fullScreenExclusiveMode_ == VK_FULL_SCREEN_EXCLUSIVE_ALLOWED_EXT) {
win32ExclusiveInfo.hmonitor = MonitorFromWindow((HWND)winsysData2_, MONITOR_DEFAULTTONEAREST);
fullScreenInfo.pNext = &win32ExclusiveInfo;
}
swap_chain_info.pNext = &fullScreenInfo;
}
#endif
res = vkCreateSwapchainKHR(device_, &swap_chain_info, NULL, &swapchain_);
if (res != VK_SUCCESS) {
ERROR_LOG(Log::G3D, "vkCreateSwapchainKHR failed! %s", VulkanResultToString(res));
return false;
}
INFO_LOG(Log::G3D, "Created swapchain: %dx%d %s", swap_chain_info.imageExtent.width, swap_chain_info.imageExtent.height, (surfCapabilities_.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_SRC_BIT) ? "(TRANSFER_SRC_BIT supported)" : "");
swapchainInited_ = true;
if (oldSwapchain != VK_NULL_HANDLE) {
vkDestroySwapchainKHR(device_, oldSwapchain, nullptr);
INFO_LOG(Log::G3D, "Destroyed old swapchain.");
}
return true;
}
void VulkanContext::SetCbGetDrawSize(std::function<VkExtent2D()> cb) {
cbGetDrawSize_ = cb;
}
VkFence VulkanContext::CreateFence(bool presignalled) {
VkFence fence;
VkFenceCreateInfo fenceInfo{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO };
fenceInfo.flags = presignalled ? VK_FENCE_CREATE_SIGNALED_BIT : 0;
vkCreateFence(device_, &fenceInfo, NULL, &fence);
return fence;
}
void VulkanContext::PerformPendingDeletes() {
for (int i = 0; i < ARRAY_SIZE(frame_); i++) {
frame_[i].deleteList.PerformDeletes(this, allocator_);
}
Delete().PerformDeletes(this, allocator_);
}
void VulkanContext::DestroyDevice() {
if (swapchain_) {
ERROR_LOG(Log::G3D, "DestroyDevice: Swapchain should have been destroyed.");
}
if (surface_) {
ERROR_LOG(Log::G3D, "DestroyDevice: Surface should have been destroyed.");
}
for (int i = 0; i < ARRAY_SIZE(frame_); i++) {
frame_[i].profiler.Shutdown();
}
INFO_LOG(Log::G3D, "VulkanContext::DestroyDevice (performing deletes)");
PerformPendingDeletes();
vmaDestroyAllocator(allocator_);
allocator_ = VK_NULL_HANDLE;
if (ownsDevice_) {
vkDestroyDevice(device_, nullptr);
}
device_ = nullptr;
}
bool VulkanContext::CreateShaderModule(const std::vector<uint32_t> &spirv, VkShaderModule *shaderModule, const char *tag) {
VkShaderModuleCreateInfo sm{ VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO };
sm.pCode = spirv.data();
sm.codeSize = spirv.size() * sizeof(uint32_t);
sm.flags = 0;
VkResult result = vkCreateShaderModule(device_, &sm, nullptr, shaderModule);
if (tag) {
SetDebugName(*shaderModule, VK_OBJECT_TYPE_SHADER_MODULE, tag);
}
if (result != VK_SUCCESS) {
return false;
} else {
return true;
}
}
EShLanguage FindLanguage(const VkShaderStageFlagBits shader_type) {
switch (shader_type) {
case VK_SHADER_STAGE_VERTEX_BIT:
return EShLangVertex;
case VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT:
return EShLangTessControl;
case VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT:
return EShLangTessEvaluation;
case VK_SHADER_STAGE_GEOMETRY_BIT:
return EShLangGeometry;
case VK_SHADER_STAGE_FRAGMENT_BIT:
return EShLangFragment;
case VK_SHADER_STAGE_COMPUTE_BIT:
return EShLangCompute;
default:
return EShLangVertex;
}
}
// Compile a given string containing GLSL into SPV for use by VK
// Return value of false means an error was encountered.
bool GLSLtoSPV(const VkShaderStageFlagBits shader_type, const char *sourceCode, GLSLVariant variant,
std::vector<unsigned int> &spirv, std::string *errorMessage) {
glslang::TProgram program;
const char *shaderStrings[1];
TBuiltInResource Resources{};
InitShaderResources(Resources);
int defaultVersion = 0;
EShMessages messages;
EProfile profile;
switch (variant) {
case GLSLVariant::VULKAN:
// Enable SPIR-V and Vulkan rules when parsing GLSL
messages = (EShMessages)(EShMsgSpvRules | EShMsgVulkanRules);
defaultVersion = 450;
profile = ECoreProfile;
break;
case GLSLVariant::GL140:
messages = (EShMessages)(EShMsgDefault);
defaultVersion = 140;
profile = ECompatibilityProfile;
break;
case GLSLVariant::GLES300:
messages = (EShMessages)(EShMsgDefault);
defaultVersion = 300;
profile = EEsProfile;
break;
default:
return false;
}
EShLanguage stage = FindLanguage(shader_type);
glslang::TShader shader(stage);
shaderStrings[0] = sourceCode;
shader.setStrings(shaderStrings, 1);
if (!shader.parse(&Resources, defaultVersion, profile, false, true, messages)) {
puts(shader.getInfoLog());
puts(shader.getInfoDebugLog());
if (errorMessage) {
*errorMessage = shader.getInfoLog();
(*errorMessage) += shader.getInfoDebugLog();
}
return false; // something didn't work
}
// TODO: Propagate warnings into errorMessages even if we succeeded here.
// Note that program does not take ownership of &shader, so this is fine.
program.addShader(&shader);
if (!program.link(messages)) {
puts(shader.getInfoLog());
puts(shader.getInfoDebugLog());
if (errorMessage) {
*errorMessage = shader.getInfoLog();
(*errorMessage) += shader.getInfoDebugLog();
}
return false;
}
// Can't fail, parsing worked, "linking" worked.
glslang::SpvOptions options;
options.disableOptimizer = false;
options.optimizeSize = false;
options.generateDebugInfo = false;
glslang::GlslangToSpv(*program.getIntermediate(stage), spirv, &options);
return true;
}
void init_glslang() {
glslang::InitializeProcess();
}
void finalize_glslang() {
glslang::FinalizeProcess();
}
// NOTE: Every vector in the class has to be listed both here and in PerformDeletes. If one is missing
// from Take, the objects in it linger on the global list until device teardown instead of being deleted
// a few frames later; if one is missing from PerformDeletes, they leak outright. Both are bugs.
void VulkanDeleteList::Take(VulkanDeleteList &del) {
// The render thread can be queueing deletes into del (the global list) while we do this.
std::lock_guard<std::mutex> lock(del.mutex_);
_dbg_assert_(cmdPools_.empty());
_dbg_assert_(descPools_.empty());
_dbg_assert_(modules_.empty());
_dbg_assert_(buffers_.empty());
_dbg_assert_(bufferViews_.empty());
_dbg_assert_(buffersWithAllocs_.empty());
_dbg_assert_(imageViews_.empty());
_dbg_assert_(imagesWithAllocs_.empty());
_dbg_assert_(deviceMemory_.empty());
_dbg_assert_(samplers_.empty());
_dbg_assert_(pipelines_.empty());
_dbg_assert_(pipelineCaches_.empty());
_dbg_assert_(renderPasses_.empty());
_dbg_assert_(framebuffers_.empty());
_dbg_assert_(pipelineLayouts_.empty());
_dbg_assert_(descSetLayouts_.empty());
_dbg_assert_(queryPools_.empty());
_dbg_assert_(callbacks_.empty());
cmdPools_ = std::move(del.cmdPools_);
descPools_ = std::move(del.descPools_);
modules_ = std::move(del.modules_);
buffers_ = std::move(del.buffers_);
buffersWithAllocs_ = std::move(del.buffersWithAllocs_);
bufferViews_ = std::move(del.bufferViews_);
imageViews_ = std::move(del.imageViews_);
imagesWithAllocs_ = std::move(del.imagesWithAllocs_);
deviceMemory_ = std::move(del.deviceMemory_);
samplers_ = std::move(del.samplers_);
pipelines_ = std::move(del.pipelines_);
pipelineCaches_ = std::move(del.pipelineCaches_);
renderPasses_ = std::move(del.renderPasses_);
framebuffers_ = std::move(del.framebuffers_);
pipelineLayouts_ = std::move(del.pipelineLayouts_);
descSetLayouts_ = std::move(del.descSetLayouts_);
queryPools_ = std::move(del.queryPools_);
callbacks_ = std::move(del.callbacks_);
del.cmdPools_.clear();
del.descPools_.clear();
del.modules_.clear();
del.buffers_.clear();
del.buffersWithAllocs_.clear();
del.bufferViews_.clear();
del.imageViews_.clear();
del.imagesWithAllocs_.clear();
del.deviceMemory_.clear();
del.samplers_.clear();
del.pipelines_.clear();
del.pipelineCaches_.clear();
del.renderPasses_.clear();
del.framebuffers_.clear();
del.pipelineLayouts_.clear();
del.descSetLayouts_.clear();
del.queryPools_.clear();
del.callbacks_.clear();
}
void VulkanDeleteList::PerformDeletes(VulkanContext *vulkan, VmaAllocator allocator) {
// Drain into a local list first. A callback is allowed to queue more deletes (~VKFramebuffer does,
// via ~VKRFramebuffer) - with the vectors already moved out, those land on an empty list and get
// performed on a later pass, rather than being appended to a vector we're iterating. It also means
// they get the normal deferral instead of being destroyed in the same pass they were queued in.
VulkanDeleteList taken;
taken.Take(*this);
deleteCount_ = taken.PerformDeletesInternal(vulkan, allocator);
}
// See the note on Take() - every vector in the class must be handled here too, or it leaks.
int VulkanDeleteList::PerformDeletesInternal(VulkanContext *vulkan, VmaAllocator allocator) {
int deleteCount = 0;
for (auto &callback : callbacks_) {
callback(vulkan);
deleteCount++;
}
callbacks_.clear();
VkDevice device = vulkan->GetDevice();
for (auto &cmdPool : cmdPools_) {
vkDestroyCommandPool(device, cmdPool, nullptr);
deleteCount++;
}
cmdPools_.clear();
for (auto &descPool : descPools_) {
vkDestroyDescriptorPool(device, descPool, nullptr);
deleteCount++;
}
descPools_.clear();
for (auto &module : modules_) {
vkDestroyShaderModule(device, module, nullptr);
deleteCount++;
}
modules_.clear();
for (auto &buf : buffers_) {
vkDestroyBuffer(device, buf, nullptr);
deleteCount++;
}
buffers_.clear();
for (auto &buf : buffersWithAllocs_) {
vmaDestroyBuffer(allocator, buf.buffer, buf.alloc);
deleteCount++;
}
buffersWithAllocs_.clear();
for (auto &bufView : bufferViews_) {
vkDestroyBufferView(device, bufView, nullptr);
deleteCount++;
}
bufferViews_.clear();
for (auto &imageWithAlloc : imagesWithAllocs_) {
vmaDestroyImage(allocator, imageWithAlloc.image, imageWithAlloc.alloc);
deleteCount++;
}
imagesWithAllocs_.clear();
for (auto &imageView : imageViews_) {
vkDestroyImageView(device, imageView, nullptr);
deleteCount++;
}
imageViews_.clear();
for (auto &mem : deviceMemory_) {
vkFreeMemory(device, mem, nullptr);
deleteCount++;
}
deviceMemory_.clear();
for (auto &sampler : samplers_) {
vkDestroySampler(device, sampler, nullptr);
deleteCount++;
}
samplers_.clear();
for (auto &pipeline : pipelines_) {
vkDestroyPipeline(device, pipeline, nullptr);
deleteCount++;
}
pipelines_.clear();
for (auto &pcache : pipelineCaches_) {
vkDestroyPipelineCache(device, pcache, nullptr);
deleteCount++;
}
pipelineCaches_.clear();
for (auto &renderPass : renderPasses_) {
vkDestroyRenderPass(device, renderPass, nullptr);
deleteCount++;
}
renderPasses_.clear();
for (auto &framebuffer : framebuffers_) {
vkDestroyFramebuffer(device, framebuffer, nullptr);
deleteCount++;
}
framebuffers_.clear();
for (auto &pipeLayout : pipelineLayouts_) {
vkDestroyPipelineLayout(device, pipeLayout, nullptr);
deleteCount++;
}
pipelineLayouts_.clear();
for (auto &descSetLayout : descSetLayouts_) {
vkDestroyDescriptorSetLayout(device, descSetLayout, nullptr);
deleteCount++;
}
descSetLayouts_.clear();
for (auto &queryPool : queryPools_) {
vkDestroyQueryPool(device, queryPool, nullptr);
deleteCount++;
}
queryPools_.clear();
return deleteCount;
}
void VulkanContext::GetImageMemoryRequirements(VkImage image, VkMemoryRequirements *mem_reqs, bool *dedicatedAllocation) {
if (Extensions().KHR_dedicated_allocation) {
VkImageMemoryRequirementsInfo2KHR memReqInfo2{VK_STRUCTURE_TYPE_IMAGE_MEMORY_REQUIREMENTS_INFO_2_KHR};
memReqInfo2.image = image;
VkMemoryRequirements2KHR memReq2 = {VK_STRUCTURE_TYPE_MEMORY_REQUIREMENTS_2_KHR};
VkMemoryDedicatedRequirementsKHR memDedicatedReq{VK_STRUCTURE_TYPE_MEMORY_DEDICATED_REQUIREMENTS_KHR};
ChainStruct(memReq2, &memDedicatedReq);
vkGetImageMemoryRequirements2(GetDevice(), &memReqInfo2, &memReq2);
*mem_reqs = memReq2.memoryRequirements;
*dedicatedAllocation =
(memDedicatedReq.requiresDedicatedAllocation != VK_FALSE) ||
(memDedicatedReq.prefersDedicatedAllocation != VK_FALSE);
} else {
vkGetImageMemoryRequirements(GetDevice(), image, mem_reqs);
*dedicatedAllocation = false;
}
}
bool IsHashMaliDriverVersion(const VkPhysicalDeviceProperties &props) {
// ARM used to put a hash in place of the driver version.
// Now they only use major versions. We'll just make a bad heuristic.
uint32_t major = VK_VERSION_MAJOR(props.driverVersion);
uint32_t branch = VK_VERSION_PATCH(props.driverVersion);
if (branch > 0)
return true;
if (branch > 100 || major > 100)
return true;
// Can (in theory) have false negatives!
return false;
}
// From Sascha's code
std::string FormatDriverVersion(const VkPhysicalDeviceProperties &props) {
if (props.vendorID == VULKAN_VENDOR_NVIDIA) {
// For whatever reason, NVIDIA has their own scheme.
// 10 bits = major version (up to r1023)
// 8 bits = minor version (up to 255)
// 8 bits = secondary branch version/build version (up to 255)
// 6 bits = tertiary branch/build version (up to 63)
uint32_t major = (props.driverVersion >> 22) & 0x3ff;
uint32_t minor = (props.driverVersion >> 14) & 0x0ff;
uint32_t secondaryBranch = (props.driverVersion >> 6) & 0x0ff;
uint32_t tertiaryBranch = (props.driverVersion) & 0x003f;
return StringFromFormat("%d.%d.%d.%d", major, minor, secondaryBranch, tertiaryBranch);
} else if (props.vendorID == VULKAN_VENDOR_ARM) {
// ARM used to just put a hash here. No point in splitting it up.
if (IsHashMaliDriverVersion(props)) {
return StringFromFormat("(hash) %08x", props.driverVersion);
}
}
// Qualcomm has an inscrutable versioning scheme. Let's just display it as normal.
// Standard scheme, use the standard macros.
uint32_t major = VK_VERSION_MAJOR(props.driverVersion);
uint32_t minor = VK_VERSION_MINOR(props.driverVersion);
uint32_t branch = VK_VERSION_PATCH(props.driverVersion);
return StringFromFormat("%d.%d.%d (%08x)", major, minor, branch, props.driverVersion);
}
std::string FormatAPIVersion(u32 version) {
return StringFromFormat("%d.%d.%d", VK_API_VERSION_MAJOR(version), VK_API_VERSION_MINOR(version), VK_API_VERSION_PATCH(version));
}
// Mainly just the formats seen on gpuinfo.org for swapchains, as this function is only used for listing
// those in the UI. Also depth buffers that we used in one place.
// Might add more in the future if we find more uses for this.
const char *VulkanFormatToString(VkFormat format) {
switch (format) {
case VK_FORMAT_A1R5G5B5_UNORM_PACK16: return "A1R5G5B5_UNORM_PACK16";
case VK_FORMAT_A2B10G10R10_UNORM_PACK32: return "A2B10G10R10_UNORM_PACK32";
case VK_FORMAT_A2R10G10B10_UNORM_PACK32: return "A2R10G10B10_UNORM_PACK32";
case VK_FORMAT_A8B8G8R8_SNORM_PACK32: return "A8B8G8R8_SNORM_PACK32";
case VK_FORMAT_A8B8G8R8_SRGB_PACK32: return "A8B8G8R8_SRGB_PACK32";
case VK_FORMAT_A8B8G8R8_UNORM_PACK32: return "A8B8G8R8_UNORM_PACK32";
case VK_FORMAT_B10G11R11_UFLOAT_PACK32: return "B10G11R11_UFLOAT_PACK32";
case VK_FORMAT_B4G4R4A4_UNORM_PACK16: return "B4G4R4A4_UNORM_PACK16";
case VK_FORMAT_B5G5R5A1_UNORM_PACK16: return "B5G5R5A1_UNORM_PACK16";
case VK_FORMAT_B5G6R5_UNORM_PACK16: return "B5G6R5_UNORM_PACK16";
case VK_FORMAT_B8G8R8A8_SNORM: return "B8G8R8A8_SNORM";
case VK_FORMAT_B8G8R8A8_SRGB: return "B8G8R8A8_SRGB";
case VK_FORMAT_B8G8R8A8_UNORM: return "B8G8R8A8_UNORM";
case VK_FORMAT_R16G16B16A16_SFLOAT: return "R16G16B16A16_SFLOAT";
case VK_FORMAT_R16G16B16A16_SNORM: return "R16G16B16A16_SNORM";
case VK_FORMAT_R16G16B16A16_UNORM: return "R16G16B16A16_UNORM";
case VK_FORMAT_R4G4B4A4_UNORM_PACK16: return "R4G4B4A4_UNORM_PACK16";
case VK_FORMAT_R5G5B5A1_UNORM_PACK16: return "R5G5B5A1_UNORM_PACK16";
case VK_FORMAT_R5G6B5_UNORM_PACK16: return "R5G6B5_UNORM_PACK16";
case VK_FORMAT_R8G8B8A8_SNORM: return "R8G8B8A8_SNORM";
case VK_FORMAT_R8G8B8A8_SRGB: return "R8G8B8A8_SRGB";
case VK_FORMAT_R8G8B8A8_UNORM: return "R8G8B8A8_UNORM";
case VK_FORMAT_D24_UNORM_S8_UINT: return "D24S8";
case VK_FORMAT_D16_UNORM: return "D16";
case VK_FORMAT_D16_UNORM_S8_UINT: return "D16S8";
case VK_FORMAT_D32_SFLOAT: return "D32f";
case VK_FORMAT_D32_SFLOAT_S8_UINT: return "D32fS8";
case VK_FORMAT_S8_UINT: return "S8";
case VK_FORMAT_UNDEFINED: return "UNDEFINED (BAD!)";
default: return "(format not added to string list)";
}
}
// I miss Rust where this is automatic :(
const char *VulkanColorSpaceToString(VkColorSpaceKHR colorSpace) {
switch (colorSpace) {
case VK_COLOR_SPACE_SRGB_NONLINEAR_KHR: return "SRGB_NONLINEAR";
case VK_COLOR_SPACE_DISPLAY_P3_NONLINEAR_EXT: return "DISPLAY_P3_NONLINEAR";
case VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT: return "EXTENDED_SRGB_LINEAR";
case VK_COLOR_SPACE_DISPLAY_P3_LINEAR_EXT: return "DISPLAY_P3_LINEAR";
case VK_COLOR_SPACE_DCI_P3_NONLINEAR_EXT: return "DCI_P3_NONLINEAR";
case VK_COLOR_SPACE_BT709_LINEAR_EXT: return "BT709_LINEAR";
case VK_COLOR_SPACE_BT709_NONLINEAR_EXT: return "BT709_NONLINEAR";
case VK_COLOR_SPACE_BT2020_LINEAR_EXT: return "BT2020_LINEAR";
case VK_COLOR_SPACE_HDR10_ST2084_EXT: return "HDR10_ST2084";
case VK_COLOR_SPACE_DOLBYVISION_EXT: return "DOLBYVISION";
case VK_COLOR_SPACE_HDR10_HLG_EXT: return "HDR10_HLG";
case VK_COLOR_SPACE_ADOBERGB_LINEAR_EXT: return "ADOBERGB_LINEAR";
case VK_COLOR_SPACE_ADOBERGB_NONLINEAR_EXT: return "ADOBERGB_NONLINEAR";
case VK_COLOR_SPACE_PASS_THROUGH_EXT: return "PASS_THROUGH";
case VK_COLOR_SPACE_EXTENDED_SRGB_NONLINEAR_EXT: return "EXTENDED_SRGB_NONLINEAR";
case VK_COLOR_SPACE_DISPLAY_NATIVE_AMD: return "DISPLAY_NATIVE_AMD";
default: return "(unknown)";
}
}