среда, 2 марта 2016 г.

Vulkan API, Queues.

Queues. Queue Family Properties

Перевод этой части спецификации несколько затянулся, связано это с тем, что объем кода для примера стал слишком большим и появилось желание это все обернуть в соответствующие классы. На сколько удачным оказалось это решение покажет время, но объем кода примеров уже снизился существенно. 
Ну что ж, приступим...



Как было рассказано в статье Vulkan API, Physical Device Queue Family, команда vkGetPhysicalDeviceQueueFamilyProperties изпользуется для получения детальной информации о семействах очередей и очередях, поддерживаемых устройством. Каждый индекс в массиве  pQueueFamilyProperties, возвращаемый этой функцией, описывает уникальное семейство очередей на физическом устройстве. Эти индексы используются при создании очередей и непосредственно соответствуют queueFamilyIndex, переданному в команду vkCreateDevice через структуру  VkDeviceQueueCreateInfo. Группировка семейств очередей на физическом устройстве зависит от конкретной реализации.


Заметка: Ожидается что физические устройства будут группировать все очереди с совпадающими свойствами в одно семейство, однако, это лишь рекомендация и возможно что физическое устройство может вернуть две отдельных очереди с одинаковыми свойствами.


Как только приложение идентифицировало физическое устройство с очередями, которые оно решило использовать, оно может создать эти очереди в сочетании с логическим устройством.


Queue Creation


Создание логического устройства так же создает и очереди, ассоциированные с этим устройством. Создаваемые очереди задаются через наборы структур VkDeviceQueueCreateInfo, которые передаются в  vkCreateDevice через  pQueueCreateInfos. Определение структуры VkDeviceQueueCreateInfo следующее:
typedef struct VkDeviceQueueCreateInfo {
VkStructureType sType;
const void * pNext;
VkDeviceQueueCreateFlags flags;
uint32_t queueFamilyIndex;
uint32_t queueCount;
const float * pQueuePriorities;
} VkDeviceQueueCreateInfo;


• sType - равен VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO.
• pNext - должен быть NULL.
• flags - зарезервирован, должен быть 0.
• queueFamilyIndex - индекс, задающий номер семейства очередей для создания на этом устройстве. Значение этого индекса соответствует индексу элемента в массиве pQueueFamilyProperties, возвращаемому vkGetPhysicalDeviceQueueFamilyProperties и должно быть меньше чем  pQueueFamilyPropertyCount.
• queueCount - должно быть больше 0 и меньше или равно значению поля queueCount структуры VkQueueFamilyProperties[queueFamilyIndex]. Задает количество создаваемых очередей данного типа.
• pQueuePriorities - массив нормализованных флоатов (значения в интервале от 0.0 до 1.0 включительно) с размером queueCount элементов, определяющих приоритеты задач, отправляемых в каждую из создаваемых очередей.


Чтоб получить хэндл на объект VkQueue, нужно вызвать команду:
void vkGetDeviceQueue(
VkDevice device,
uint32_t queueFamilyIndex,
uint32_t queueIndex,
VkQueue * pQueue);
• device - хэндл логического устройства, владеющего очередью.
• queueFamilyIndex - индекс семейства очередей, к которому эта очередь принадлежит.
• queueIndex - индекс очереди, которую необходимо получить, внутри семейства очередей.
• pQueue - указатель на объект VkQueue, куда будет возращен хэндл запрашиваемой очереди.


Queue Family Index


Индекс семейства очередей (в дальнейшем просто индекс) используется в нескольких случаях:
  • Когда мы получаем указатель на очередь через vkGetDeviceQueue, индекс используется чтоб выбрать какая из семейств очередей вернет хэндл  хэндл VkQueue, описанным выше способом.
  • Когда создается объект VkCommandPool, индекс указывается в структуре VkCommandPoolCreateInfo. Командные буферы из этого пула могут быть отправлены только в очереди соответствующего семейства.
  • Когда создаются ресурсы VkImage и VkBuffer, набор семейства очередей включается в структуры VkImageCreateInfo и VkBufferCreateInfo, чтоб указать семейства очередей, имеющих доступ к этим ресурсам.
  • Когда вставляется VkBufferMemoryBarrier или VkImageMemoryBarrier, индекс исходного и конечного семейства очередей указывает на допустимость владения буфером или изображением, передаваемыми из одного семейства в другое.


Queue Priority


За каждой очередью через структуру VkDeviceQueueCreateInfo, указываемую при создании устройства, закрепляется приоритет. Приоритет это нормированное значение  (от 0 до 1), которое впоследствии транслируется в дискретное значение приоритета (зависит от реализации), при этом наинизший приоритет имеет значение 0.0, наивысший 1.0. Внутри одного логического устройства очередям с большим приоритетом выделяется больше процессорного времени чем очередям с низким приоритетом. Реализация не дает никаких гарантий в отношении порядка и расписания работы очередей с одинаковым приоритетом, кроме случаев когда явно определены примитивы планирования. Реализация не дает никаких гарантий относительно приоритетов очередей, принадлежащих разным устройствам. Реализация может позволить очереди с более высоким приоритетом истощать ресурсы очереди с более низким приоритетом на том же устройстве VkDevice, пока в очереди с более высоким приоритетом не закончатся команды. Приоритет очереди на одном устройстве никоим образом не влияет на потребление ресурсов очередей на другом устройстве. Нет никаких особых гарантий того. что очередь с более высоким приоритетом получит больше процессорного времени или лучшее качество обслуживания чем очереди с низким приоритетом.

Queue Synchronization


Чтоб дождаться завершения всех задач внутри одной очереди необходимо вызвать:
VkResult vkQueueWaitIdle(VkQueue queue);
• queue - очередь, чье завершение нужно дождаться.


vkQueueWaitIdle блокирует выполнение пока все буферы команд и операции “sparse binding” в указанной очереди не будут завершены.


Возможные коды ошибок:
Success
• VK_SUCCESS
Failure
• VK_ERROR_OUT_OF_HOST_MEMORY
• VK_ERROR_OUT_OF_DEVICE_MEMORY
• VK_ERROR_DEVICE_LOST


Синхронизация между очередями выполняется с использованием семафоров Вулкана.


Sparse Memory Binding


//Перевод отвратительный, так как пока плохо представляю контекст проблемы
Вулкан предоставляет возможность привязки распределенной (Sparse) памяти к буферам и изображениям, привязка такой памяти это операция очереди. Очередь, в которой установлен флаг  VK_QUEUE_SPARSE_BINDING_BIT должна быть способна к поддержке маппинга виртуальных адресов в физическое адресное пространство устройства. Это вызывает обновление таблицы страниц отображения “page table mappings” на устройстве, которое должно быть синхронизировано в очереди для избежания повреждения таблицы страниц отображения пока выполняются графические команды. Привязывая распределенные ресурсы памяти к очереди, все команды, зависящие от обновления привязок, синхронизируются так, чтоб их выполнение было только после завершения обновления привязок.


Queue Destruction


Очереди создаются вместе с логическим устройством через vkCreateDevice. Все связанные с логическим устройством очереди удаляются вместе с устройством, когда для этого устройства вызывается vkDestroyDevice.


=====================================================================


В данной главе, а так же главе "Vulkan API. Devices" рассказывалось о создании логического устройства и очереди, связанной с этим устройством, одно без другого существовать не может.


Количество кода, необходимо для инициализации физического устройства уже слишком велико для примеров, поэтому, для сокращения примеров и избегания рутинных процедур, все рассмотренные ранее сущности были обернуты в простые классы, их сорцы прилагаются к статье.


И так, чтоб получить хэндл нашей очереди, нам необходимо вызвать функцию vkGetDeviceQueue, которой нужно скормить хэндл логического устройства. Но чтоб его создать командой vkCreateDevice необходимо заполнить две дополнительные структуры VkDeviceCreateInfo и VkDeviceQueueCreateInfo.


Структура VkDeviceCreateInfo частично нам уже знакома по созданию инстанса, мы должны обязательно указать тип данной структуры (sType), передать указатель на структуру VkDeviceQueueCreateInfo (рассмотрим чуть ниже) и указать создание как минимум одной очереди (queueCreateInfoCount = 1). Поля *LayerNames и *ExtensionNames мы пока так же оставим пустыми, пока не разберемся с этими расширениями. Отдельно стоит упомянуть свойство pEnabledFeatures. Это свойство задает какие из дополнительных возможностей будут доступны. Таких свойств очень много и рассматриваются они намного позже. Так как мы пока что ничего рисовать не будем, то это поле можно оставить пустым.


Таким образом, в простейшем случае, эта структура будет выглядеть так:


VkDeviceCreateInfo dev_info = {};
dev_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
dev_info.pNext = NULL;
dev_info.flags = 0;
dev_info.queueCreateInfoCount = 1;
dev_info.pQueueCreateInfos = &queue_info;
dev_info.enabledLayerCount = 0;
dev_info.ppEnabledLayerNames = NULL;
dev_info.enabledExtensionCount = 0;
dev_info.ppEnabledExtensionNames = NULL;
dev_info.pEnabledFeatures = NULL;


Теперь разберем вторую структуру, VkDeviceQueueCreateInfo, указатель на которую нам нужно передать в dev_info.pQueueCreateInfos. Так же как и в предыдущем случае, нам нужно первым делом указать тип этой структуры (sType), в обязательном порядке мы должны указать индекс семейства очередей (queueFamilyIndex), которую мы хотим использовать. В этой главе было подробно рассказано что это такое и где используется, о том как получить список доступных семейств и их свойств было рассказано в главе “Physical Device Queue Family”. Фактически выбор семейства сводится к перебору всех доступных и выборе того, который подходит для ваших задач - рисование, вычисления, загрузка данных или работа с разряженными ресурсами. В своем примере я эту функцию поместил в класс физического устройства, она мне вернула индекс в переменную qGCIndex (queue Graphics | Compute Index). Так же необходимо указать сколько очередей мы хотим получить, для примера нам хватит и одной, что мы и указываем в queueCount. Однако, даже если у нас одна очередь,то мы все равно вынуждены указать ее приоритет (pQueuePriorities), это указатель на массив флоатов, в котором количество элементов совпадает с количеством создаваемых очередей. Заполненная структура VkDeviceQueueCreateInfo выглядит так:
float queuePriorities[] = { 1.0f };
VkDeviceQueueCreateInfo queue_info = {};
queue_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queue_info.pNext = NULL;
queue_info.flags = 0;
queue_info.queueFamilyIndex = qGCIndex;
queue_info.queueCount = 1;
queue_info.pQueuePriorities = queuePriorities;


После того как мы заполнили эти структуры данными, мы можем попробовать создать логическое устройство:


VkDevice log_dev;
err = vkCreateDevice(*dev->getPhysicalDevice(), &dev_info, NULL, &log_dev);


Функция getPhysicalDevice() реализована в моем классе VK::PhysicalDevice, она просто возвращает хэндл физического устройства VkPhysicalDevice, созданного ранее.


Создав это логическое устройство мы можем запросить у него нашу очередь:


VkQueue GCqueue;
vkGetDeviceQueue(log_dev, qGCIndex, 0, &GCqueue);


Здесь так же все просто, передаем хэндл логического устройства, полученный выше, передаем индекс нужного нам семейства и номер созданной нами для устройства очереди (мы создавали только одну очередь, поэтому ее номер 0).


Ну вот собственно и все, теперь мы можем использовать свеже-созданную очередь для своих целей, к примеру - убедиться что очередь простаивает, прежде чем мы попытаемся ее удалить:


err = vkQueueWaitIdle(GCqueue);


Эта функция заблокирует поток до тех пор, пока все задачи, поставленные в очередь, не будут завершены. Формально, для данной задачи (удаления логического устройства), нам этого делать не нужно, так как функция vkDeviceWaitIdle делает то же самое, ожидая завершения всех очередей, связанных с устройством, но надо же было проверить что очередь создана успешно и работает :)


Теперь можно переходить к завершающей стадии, дождаться пока освободится устройство и удалить его:


err = vkDeviceWaitIdle(log_dev);
vkDestroyDevice(log_dev, NULL);


Все, на этом работа с устройством завершена. Данный пример вы можете найти здесь.


P.S. Так как для работы нам может потребоваться множество очередей и каждый раз выполнять всю эту процедуру очень громоздко, то я так же обернул это в соответствующие классы? таким образом, приведенный выше код теперь выглядит как:


float queuePriorities[] = { 1.0f };
VkDeviceQueueCreateInfo queue_info = {};
VK::FillDeviceQueueCreateInfo(queue_info, qGCIndex, 1, queuePriorities);


VkDeviceCreateInfo dev_info = {};
VK::FillDeviceCreateInfo(dev_info, 1, &queue_info);


VK::Device const *log_dev = dev->createDevice(&dev_info);


VkQueue const * GCqueue = log_dev->getDeviceQueue(qGCIndex, 0);


if (GCqueue) {
VkResult err = vkQueueWaitIdle(*GCqueue);
if (err != VK_SUCCESS) VK::ErrorCallback(err, AT, "Queue Idle failed");
else printf("\nQueue is Idle\n");
}

Соответствующий код вы можете найти здесь.

Комментариев нет:

Отправить комментарий