Главная Статьи Ссылки Скачать Скриншоты Юмор Почитать Tools Проекты Обо мне Гостевая Форум |
"Begin at the beginning" the King said gravely"and go on till you come to the end: then stop
После создания экземпляра (вместе со списком слоев валидации и списком расширений), а также настройки debug callback нас ждет работа с устройством и очередями.
На самом деле Vulkan различает физические и логические устройства. Физическое устройство (physical device) это реальный GPU, подключенный к системе. И в системе их может быть несколько - например у вас установлен дискретный GPU, но также есть и интегрированный GPU. И вы можете одновременно работать сразу с несколькими GPU, но мы далее будем рассматривать работу только с одним. Логическое устройство это как интерфейс, через который вы можете работать с физическим устройством. Обратите внимание, что у вас может быть сразу несколько логических устройств связанных с одним физическим.
После того, как вы выбрали физическое устройство, то нужно для него создать логическое устройство, с которым вы и будете дальше работать.
Для начала мы просто соберем информацию обо всех доступных физических устройствах в системе.
Для этого мы воспользуемся функцией vkEnumeratPhysicalDevices
.
Как и с остальными функциями vkEnumerate
мы вызовем ее два раза - один раз для получения числа физических устройств в системе
и второй - для получения списка самих устройств.
Обратите внимание, что каждому физическому устройству соответствует значение типа VkPhysicalDevice
.
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
uint32_t deviceCount = 0;
// get number of physical device in system
vkEnumeratePhysicalDevices ( instance, &deviceCount, nullptr );
// allocate memory for them
std::vector<VkPhysicalDevice> physDevices ( deviceCount );
// get actual devices
vkEnumeratePhysicalDevices ( instance, &deviceCount, physDevices.data () );
std::cout << "Found " << deviceCount << "physical devices" << std::endl;
Дальнейшим шагом после получения списка всех физических устройств будет получение их свойств (properties) и возможностей (features). Следующий фрагмент кода получает свойства и возможности для каждого найденного устройства и выводит некоторые из них.
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
uint32_t deviceCount = 0;
// get number of physical devices in the system
vkEnumeratePhysicalDevices ( instance, &deviceCount, nullptr );
// allocate memory for them
std::vector<VkPhysicalDevice> physDevices ( deviceCount );
// get actual devices
vkEnumeratePhysicalDevices ( instance, &deviceCount, physDevices.data () );
std::cout << "Found " << deviceCount << "physical devices" << std::endl;
// print some info about each device in turn
for (const auto& device : physDevices )
{
VkPhysicalDeviceProperties deviceProperties;
VkPhysicalDeviceFeatures deviceFeatures;
vkGetPhysicalDeviceProperties ( device, &deviceProperties );
vkGetPhysicalDeviceFeatures ( device, &deviceFeatures );
std::cout << deviceProperties.deviceName
<< (deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU ?
" discrete" : " embedded" ) << std::endl;
if ( deviceFeatures.geometryShader )
std::cout << "Geometry shaders supported" << std::endl;
if ( deviceFeatures.tessellationShader )
std::cout << "Tessellation shaders supported" << std::endl;
}
Все запросы на рендеринг, которые мы будем отправлять на GPU, будут помещаться в одну из очередей GPU.
На самом деле каждый GPU имеет много различных очередей и они группируются в семейства (families) очередей.
Семейство очередей объединяет очереди с одинаковыми свойствами.
Разные семейства очередей могут обрабатывать разные запросы к GPU, такие как графические команды, счетные задачи, копирование данных и т.п.
При помощи функции vkGetPhysicalDeviceQueueFamilyProperties
(очень похожей на рассмотренные ранее функции vkEnumerate
)
мы можем для каждого физического устройства узнать сколько у него есть различных семейств очередей и какие свойства у каждого семейства.
uint32_t queueFamilyCount = 0;
// get number of queue families for physical device
vkGetPhysicalDeviceQueueFamilyProperties ( physicalDevice, &queueFamilyCount, nullptr );
// allocate memory for them
std::vector<VkQueueFamilyProperties> queueFamilies ( queueFamilyCount );
// get actual info on all families
vkGetPhysicalDeviceQueueFamilyProperties ( physicalDevice, &queueFamilyCount, queueFamilies.data () );
std::cout << "Queue families " << queueFamilyCount << std::endl;
// dump info about each family in turn
for ( const auto& family : queueFamilies )
{
std::cout << "\tCount " << family.queueCount;
// does the queues support graphics
if ( family.queueFlags & VK_QUEUE_GRAPHICS_BIT )
std::cout << " Graphics ";
// does the queues support compute
if ( family.queueFlags & VK_QUEUE_COMPUTE_BIT )
std::cout << " Compute";
// does the queues support memory transfer
if ( family.queueFlags & VK_QUEUE_TRANSFER_BIT )
std::cout << " Transfer";
std::cout << std::endl;
}
Вот информация, выданная для моего GPU (GeForce 3060 RTX):
Queue families 3
Count 16 Graphics Compute Transfer
Count 2 Transfer
Count 8 Compute Transfer
Теперь мы знаем как получить всю информацию, позволяющую нам выбрать подходящее физическое устройство. В простейшем варианте мы перебираем все имеющиеся физические устройства, пока не найдем первое, удовлетворяющее некоторому критерию.
Есть и более гибкая стратегия - мы разделяем все свойства и возможности на обязательные и желательные. Далее для каждого устройства, обладающего всеми обязательными характеристиками, по строим по необязательным некоторый рейтинг устройства - насколько оно хорошо нам подходит. Потом мы просто выбираем из списка устройство с наибольшим рейтингом. В примере ниже мы требуем наличия хотя бы одного семейства очередей с поддержкой вычислений (compute).
int isDeviceSuitable ( VkPhysicalDevice device )
{
uint32_t queueFamilyCount = 0;
int computeFamily = -1;
int i = 0;
// get number of queue families
vkGetPhysicalDeviceQueueFamilyProperties ( device, &queueFamilyCount, nullptr );
// reserve memory of info on all famiies
std::vector<VkQueueFamilyProperties> queueFamilies ( queueFamilyCount );
// get actual families properties
vkGetPhysicalDeviceQueueFamilyProperties ( device, &queueFamilyCount, queueFamilies.data () );
// check whether family supports compute
for ( const auto& queueFamily : queueFamilies )
{
if ( computeFamily == -1 && queueFamily.queueFlags & VK_QUEUE_COMPUTE_BIT )
return i;
i++;
}
return -1;
}
// . . .
int computeFamily = -1;
// look for first device which mets our requirements (compute)
for ( const auto& dev : physDevices )
if ( (computeFamily = isDeviceSuitable ( dev ) ) > -1 )
{
physicalDevice = dev;
break;
}
// check whether we have found any physical device
if ( physicalDevice == VK_NULL_HANDLE )
fatal () << "VulkanWindow: failed to find a suitable GPU!";
После выбора физического устройства мы должны на его основе создать соответствующее логическое устройство для работы. Обратите внимание, что можно для одного физического устройства создать несколько логических. Для создания логического устройства надо не только указать соответствующее физическое устройство, но также задать требуемые очереди и возможности устройства.
Для задания каждой требуемой очереди служит структура VkDeviceQueueCreateInfo
.
В ней мы задаем индекс семейства, к которому должна принадлежать создаваемая очередь, сколько таких очередей мы хотим создать и массив приоритетов для каждой создаваемой очереди.
На самом деле обычно не имеет смысла создавать более одной очереди для каждой цели, поэтому мы всегда будем создавать по одной очереди каждого требуемого типа.
VkDeviceQueueCreateInfo queueCreateInfo = {};
float queuePriorities [] = { 1.0f };
queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queueCreateInfo.queueFamilyIndex = computeFamily;
queueCreateInfo.queueCount = 1;
queueCreateInfo.pQueuePriorities = &queuePriority;
В качестве требуемых возможностей устройства самый простой выбор - просто указать все возможности, соответствующего устройства, полученные ранее.
Итак, для создания логического устройства нам надо передать рассмотренные ранее параметры в функцию vkCreateDevice
, задав их в полях структуры VkDeviceCreateInfo
.
Для случая, когда нам нужна только очередь для счета (compute queue) мы получаем следующий код:
VkDeviceCreateInfo devCreateInfo = {};
VkDevice device = VK_NULL_HANDLE;
devCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
devCreateInfo.pQueueCreateInfos = &queueCreateInfo;
devCreateInfo.queueCreateInfoCount = 1;
devCreateInfo.pEnabledFeatures = &deviceFeatures;
devCreateInfo.enabledExtensionCount = 0;
devCreateInfo.ppEnabledExtensionNames = nullptr;
devCreateInfo.enabledLayerCount = 0;
if ( vkCreateDevice ( physicalDevice, &devCreateInfo, nullptr, &device ) != VK_SUCCESS )
fatal () << "VulknaWindow: failed to create logical device!";
Теперь нам осталось только получить созданные для нашего логического устройства очереди - нет необходимости их явно создавать, они уже были автоматически созданы при создании логического устройства.
Каждая очередь задается значением типа VkQueue
.
VkQueue computeQueue = VK_NULL_HANDLE;
vkGetDeviceQueue ( device, computeFamily, 0, &computeQueue );
Код к этой статье (исходный код на С++ и проект для cmake) можно скачать по этой ссылке.