blob: bf7939c6aaca8c6cd1857460faf3aba6542f1f34 [file] [log] [blame]
/*
* Copyright 2025 Rive
*/
#include <string>
#include "rive/renderer/vulkan/vkutil.hpp"
#include "rive_vk_bootstrap/vulkan_device.hpp"
#include "rive_vk_bootstrap/vulkan_instance.hpp"
#include "logging.hpp"
#include "vulkan_library.hpp"
namespace rive_vkb
{
class VulkanInstance;
static const char* physicalDeviceTypeName(VkPhysicalDeviceType type)
{
switch (type)
{
case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU:
return "Integrated";
case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU:
return "Discrete";
case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU:
return "Virtual";
case VK_PHYSICAL_DEVICE_TYPE_CPU:
return "CPU";
default:
return "Other";
}
}
VulkanDevice::VulkanDevice(VulkanInstance& instance, const Options& opts)
{
assert(!opts.headless ||
opts.presentationSurfaceForDeviceSelection == VK_NULL_HANDLE &&
"It doesn't make sense to specify a presentation surface for a "
"headless device");
const char* nameFilter = opts.gpuNameFilter;
if (const char* gpuFromEnv = getenv("RIVE_GPU"); gpuFromEnv != nullptr)
{
// Override the program's GPU filter with one from the environment if
// it's set.
nameFilter = gpuFromEnv;
}
auto findResult = findCompatiblePhysicalDevice(
instance,
nameFilter,
opts.presentationSurfaceForDeviceSelection);
m_physicalDevice = findResult.physicalDevice;
DEFINE_AND_LOAD_INSTANCE_FUNC(vkGetPhysicalDeviceFeatures, instance);
assert(vkGetPhysicalDeviceFeatures != nullptr);
VkPhysicalDeviceFeatures supportedFeatures;
vkGetPhysicalDeviceFeatures(m_physicalDevice, &supportedFeatures);
VkPhysicalDeviceFeatures requestedFeatures = {
.independentBlend =
!opts.coreFeaturesOnly && supportedFeatures.independentBlend,
// We use wireframe for debugging, so enable it even in core mode
.fillModeNonSolid = supportedFeatures.fillModeNonSolid,
// Always enable this
.fragmentStoresAndAtomics = supportedFeatures.fragmentStoresAndAtomics,
.shaderClipDistance =
!opts.coreFeaturesOnly && supportedFeatures.shaderClipDistance,
};
m_riveVulkanFeatures = {
.apiVersion = instance.apiVersion(),
.independentBlend = bool(requestedFeatures.independentBlend),
.fillModeNonSolid = bool(requestedFeatures.fillModeNonSolid),
.fragmentStoresAndAtomics =
bool(requestedFeatures.fragmentStoresAndAtomics),
.shaderClipDistance = bool(requestedFeatures.shaderClipDistance),
};
DEFINE_AND_LOAD_INSTANCE_FUNC(vkEnumerateDeviceExtensionProperties,
instance);
assert(vkEnumerateDeviceExtensionProperties != nullptr);
std::vector<VkExtensionProperties> supportedExtensions;
{
uint32_t count;
vkEnumerateDeviceExtensionProperties(m_physicalDevice,
nullptr,
&count,
nullptr);
supportedExtensions.resize(count);
vkEnumerateDeviceExtensionProperties(m_physicalDevice,
nullptr,
&count,
supportedExtensions.data());
}
std::vector<const char*> addedExtensions;
// This extension *must* be enabled if it's supported (it's usually on a
// device that is not a fully-conforming device)
// TODO: we may want to note that a device had this extension, it might be
// useful information for devices that are doing weird things.
addExtensionIfSupported("VK_KHR_portability_subset",
supportedExtensions,
addedExtensions);
if (!opts.headless)
{
if (!addExtensionIfSupported(VK_KHR_SWAPCHAIN_EXTENSION_NAME,
supportedExtensions,
addedExtensions))
{
LOG_ERROR_LINE("Cannot create device: %s is not supported.",
VK_KHR_SWAPCHAIN_EXTENSION_NAME);
abort();
}
}
// If this has a value we'll use it in the VkDeviceCreateInfo chain
std::optional<VkPhysicalDeviceRasterizationOrderAttachmentAccessFeaturesEXT>
rasterOrderFeatures;
if (!opts.coreFeaturesOnly)
{
rasterOrderFeatures = tryEnableRasterOrderFeatures(instance,
supportedExtensions,
addedExtensions);
}
// Get our list of queue family properties.
{
DEFINE_AND_LOAD_INSTANCE_FUNC(vkGetPhysicalDeviceQueueFamilyProperties,
instance);
uint32_t count;
vkGetPhysicalDeviceQueueFamilyProperties(m_physicalDevice,
&count,
nullptr);
m_queueFamilyProperties.resize(count);
vkGetPhysicalDeviceQueueFamilyProperties(
m_physicalDevice,
&count,
m_queueFamilyProperties.data());
}
// Now find the graphics queue in the list.
m_graphicsQueueFamilyIndex = std::numeric_limits<uint32_t>::max();
for (uint32_t i = 0; i < m_queueFamilyProperties.size(); i++)
{
if ((m_queueFamilyProperties[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) !=
0)
{
m_graphicsQueueFamilyIndex = i;
break;
}
}
assert(m_graphicsQueueFamilyIndex != std::numeric_limits<uint32_t>::max() &&
"Could not find graphics queue index");
// We're going to create a single queue (the graphics queue) with a priority
// of 1.
float queuePriority = 1.0f;
VkDeviceQueueCreateInfo queueCreateInfo = {
.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
.queueFamilyIndex = m_graphicsQueueFamilyIndex,
.queueCount = 1,
.pQueuePriorities = &queuePriority,
};
// Finally create the actual device.
VkDeviceCreateInfo deviceCreateInfo = {
.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
.pNext = (rasterOrderFeatures.has_value())
? &rasterOrderFeatures.value()
: nullptr,
.queueCreateInfoCount = 1,
.pQueueCreateInfos = &queueCreateInfo,
.enabledExtensionCount = uint32_t(addedExtensions.size()),
.ppEnabledExtensionNames = addedExtensions.data(),
.pEnabledFeatures = &requestedFeatures,
};
DEFINE_AND_LOAD_INSTANCE_FUNC(vkCreateDevice, instance);
VK_CHECK(vkCreateDevice(m_physicalDevice,
&deviceCreateInfo,
nullptr,
&m_device));
LOAD_REQUIRED_MEMBER_INSTANCE_FUNC(vkGetDeviceProcAddr, instance);
LOAD_REQUIRED_MEMBER_INSTANCE_FUNC(vkDeviceWaitIdle, instance);
m_vkGetDeviceQueue = reinterpret_cast<PFN_vkGetDeviceQueue>(
m_vkGetDeviceProcAddr(m_device, "vkGetDeviceQueue"));
m_vkDestroyDevice = reinterpret_cast<PFN_vkDestroyDevice>(
m_vkGetDeviceProcAddr(m_device, "vkDestroyDevice"));
LOAD_MEMBER_INSTANCE_FUNC(vkGetPhysicalDeviceSurfaceFormatsKHR, instance);
LOAD_MEMBER_INSTANCE_FUNC(vkGetPhysicalDeviceSurfacePresentModesKHR,
instance);
LOAD_MEMBER_INSTANCE_FUNC(vkGetPhysicalDeviceSurfaceCapabilitiesKHR,
instance);
printf("==== Vulkan %i.%i.%i GPU (%s): %s [ ",
VK_API_VERSION_MAJOR(m_riveVulkanFeatures.apiVersion),
VK_API_VERSION_MINOR(m_riveVulkanFeatures.apiVersion),
VK_API_VERSION_PATCH(m_riveVulkanFeatures.apiVersion),
physicalDeviceTypeName(findResult.deviceType),
findResult.deviceName.c_str());
struct CommaSeparator
{
const char* m_separator = "";
const char* operator*() { return std::exchange(m_separator, ", "); }
} commaSeparator;
if (m_riveVulkanFeatures.independentBlend)
printf("%sindependentBlend", *commaSeparator);
if (m_riveVulkanFeatures.fillModeNonSolid)
printf("%sfillModeNonSolid", *commaSeparator);
if (m_riveVulkanFeatures.fragmentStoresAndAtomics)
printf("%sfragmentStoresAndAtomics", *commaSeparator);
if (m_riveVulkanFeatures.shaderClipDistance)
printf("%sshaderClipDistance", *commaSeparator);
if (m_riveVulkanFeatures.rasterizationOrderColorAttachmentAccess)
printf("%srasterizationOrderColorAttachmentAccess", *commaSeparator);
printf(" ] ====\n");
}
VulkanDevice::~VulkanDevice() { m_vkDestroyDevice(m_device, nullptr); }
VkSurfaceCapabilitiesKHR VulkanDevice::getSurfaceCapabilities(
VkSurfaceKHR surface) const
{
VkSurfaceCapabilitiesKHR caps;
VK_CHECK(m_vkGetPhysicalDeviceSurfaceCapabilitiesKHR(m_physicalDevice,
surface,
&caps));
return caps;
}
VulkanDevice::FindDeviceResult VulkanDevice::findCompatiblePhysicalDevice(
VulkanInstance& instance,
const char* nameFilter,
VkSurfaceKHR optionalSurfaceForValidation)
{
if (nameFilter != nullptr && nameFilter[0] == '\0')
{
// Clear name filter if it's empty string.
nameFilter = nullptr;
}
auto desiredDeviceType = VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU;
if (nameFilter != nullptr &&
(strcmp(nameFilter, "integrated") == 0 || strcmp(nameFilter, "i") == 0))
{
// "i" and "integrated" are special-case nameFilters that mean "use the
// integrated GPU".
desiredDeviceType = VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU;
nameFilter = nullptr;
}
std::vector<VkPhysicalDevice> physicalDevices;
{
DEFINE_AND_LOAD_INSTANCE_FUNC(vkEnumeratePhysicalDevices, instance);
assert(vkEnumeratePhysicalDevices != nullptr);
uint32_t count;
VK_CHECK(
vkEnumeratePhysicalDevices(instance.vkInstance(), &count, nullptr));
physicalDevices.resize(count);
VK_CHECK(vkEnumeratePhysicalDevices(instance.vkInstance(),
&count,
physicalDevices.data()));
}
DEFINE_AND_LOAD_INSTANCE_FUNC(vkGetPhysicalDeviceProperties, instance);
assert(vkGetPhysicalDeviceProperties != nullptr);
DEFINE_AND_LOAD_INSTANCE_FUNC(vkGetPhysicalDeviceSurfaceFormatsKHR,
instance);
DEFINE_AND_LOAD_INSTANCE_FUNC(vkGetPhysicalDeviceSurfacePresentModesKHR,
instance);
auto IsSurfaceSupported = [&](VkPhysicalDevice device) {
uint32_t surfaceFormatCount = 0;
vkGetPhysicalDeviceSurfaceFormatsKHR(device,
optionalSurfaceForValidation,
&surfaceFormatCount,
nullptr);
uint32_t presentModeCount = 0;
vkGetPhysicalDeviceSurfacePresentModesKHR(device,
optionalSurfaceForValidation,
&presentModeCount,
nullptr);
// We don't care *what* formats/modes are returned, we just need to
// know that there would be any that are supported.
return surfaceFormatCount > 0 && presentModeCount > 0;
};
if (nameFilter != nullptr)
{
// Find a device containing the given filter
FindDeviceResult matchResult = {
.physicalDevice = VK_NULL_HANDLE,
};
std::vector<std::string> matchedDeviceNames;
for (const auto& device : physicalDevices)
{
if (optionalSurfaceForValidation != VK_NULL_HANDLE &&
!IsSurfaceSupported(device))
{
// This device does not support the surface we want to
// present with.
continue;
}
VkPhysicalDeviceProperties props{};
vkGetPhysicalDeviceProperties(device, &props);
if (strstr(props.deviceName, nameFilter) != nullptr)
{
matchResult = {
.physicalDevice = device,
.deviceName = props.deviceName,
.deviceType = props.deviceType,
};
matchedDeviceNames.push_back(std::string{props.deviceName});
}
}
if (matchedDeviceNames.size() > 1)
{
LOG_ERROR_LINE("Cannot create device: Too many GPU matches for "
"filter '%s':",
nameFilter);
for (auto& matchName : matchedDeviceNames)
{
LOG_ERROR_LINE(" '%s'", matchName.c_str());
}
LOG_ERROR_LINE("Please update the RIVE_GPU environment variable.");
abort();
}
if (matchResult.physicalDevice != VK_NULL_HANDLE)
{
return matchResult;
}
LOG_ERROR_LINE(
"Cannot create device: No GPU matches for filter '%s'.\nPlease "
"update the RIVE_GPU environment variable.",
nameFilter);
abort();
}
else
{
// Without a nameFilter we are going to search for any device, but do a
// first pass looking for the desired type of devices only.
for (auto onlyAcceptDesiredType : {true, false})
{
for (const auto& device : physicalDevices)
{
VkPhysicalDeviceProperties props{};
vkGetPhysicalDeviceProperties(device, &props);
if (optionalSurfaceForValidation != VK_NULL_HANDLE &&
!IsSurfaceSupported(device))
{
// This device does not support the surface we want to
// present with.
continue;
}
if (!onlyAcceptDesiredType ||
props.deviceType == desiredDeviceType)
{
return {
.physicalDevice = device,
.deviceName = props.deviceName,
.deviceType = props.deviceType,
};
}
}
}
LOG_ERROR_LINE(
"Cannot create device: no supported GPU devices detected.");
abort();
}
}
std::optional<VkPhysicalDeviceRasterizationOrderAttachmentAccessFeaturesEXT>
VulkanDevice::tryEnableRasterOrderFeatures(
VulkanInstance& instance,
const std::vector<VkExtensionProperties>& supportedExtensions,
std::vector<const char*>& extensions)
{
// Attempt to enable rasterization order attachment access, both by its
// standard name and by the AMD-specific name
for (auto* ext :
{VK_EXT_RASTERIZATION_ORDER_ATTACHMENT_ACCESS_EXTENSION_NAME,
"VK_AMD_rasterization_order_attachment_access"})
{
if (addExtensionIfSupported(ext, supportedExtensions, extensions))
{
constexpr static VkPhysicalDeviceRasterizationOrderAttachmentAccessFeaturesEXT
requestedRasterOrderFeatures = {
.sType =
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RASTERIZATION_ORDER_ATTACHMENT_ACCESS_FEATURES_EXT,
.rasterizationOrderColorAttachmentAccess = VK_TRUE,
};
auto testedRasterOrderFeatures = requestedRasterOrderFeatures;
// Test to see if this is supported
VkPhysicalDeviceFeatures2 features = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
.pNext = &testedRasterOrderFeatures,
};
if (instance.tryGetPhysicalDeviceFeatures2(m_physicalDevice,
&features) &&
testedRasterOrderFeatures
.rasterizationOrderColorAttachmentAccess)
{
// The query came back with the requested flag set so return the
// feature set we want, it's supported!
m_riveVulkanFeatures.rasterizationOrderColorAttachmentAccess =
true;
return requestedRasterOrderFeatures;
}
break;
}
}
return std::nullopt;
}
bool VulkanDevice::addExtensionIfSupported(
const char* name,
const std::vector<VkExtensionProperties>& supportedExtensions,
std::vector<const char*>& extensions)
{
for (const auto& ext : supportedExtensions)
{
if (strcmp(ext.extensionName, name) == 0)
{
extensions.push_back(name);
return true;
}
}
return false;
}
std::vector<VkSurfaceFormatKHR> VulkanDevice::getSurfaceFormats(
VkSurfaceKHR surface)
{
std::vector<VkSurfaceFormatKHR> formats;
uint32_t count;
m_vkGetPhysicalDeviceSurfaceFormatsKHR(m_physicalDevice,
surface,
&count,
nullptr);
formats.resize(count);
m_vkGetPhysicalDeviceSurfaceFormatsKHR(m_physicalDevice,
surface,
&count,
formats.data());
return formats;
}
std::vector<VkPresentModeKHR> VulkanDevice::getSurfacePresentModes(
VkSurfaceKHR surface)
{
std::vector<VkPresentModeKHR> modes;
uint32_t count;
m_vkGetPhysicalDeviceSurfacePresentModesKHR(m_physicalDevice,
surface,
&count,
nullptr);
modes.resize(count);
m_vkGetPhysicalDeviceSurfacePresentModesKHR(m_physicalDevice,
surface,
&count,
modes.data());
return modes;
}
void VulkanDevice::waitUntilIdle() const { m_vkDeviceWaitIdle(m_device); }
} // namespace rive_vkb