четверг, 17 марта 2016 г.

Vulkan, еще раз о синхронизации...

Наконец-то я закончил перевод этого здоровенного и не менее важного куска спецификации. О том, что я думаю об авторе этого раздела спецификации я уже писал много, потому я просто счастлив что это осталось позади (хотя кто знает что еще меня ждет впереди))


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



Итак, мы уже знаем что Вулкан переложил всю работу по синхронизации с драйвера на приложение, сам же драйвер теперь ничего не гарантирует, команды могут выполняться в любом порядке, причем даже параллельно, завершаться они так же могут в любом порядке. Чтоб навести какой-то порядок в этом были добавлены специальные команды синхронизации. Синхронизация преследует две основных цели:
  1. убедиться что соблюдается порядок выполнения команд (если команда B зависит от результатов работы команды A, то нам нужно убедиться что команда B будет выполнена только после того как завершится команда A), так называемое execution dependency (зависимость выполнения).
  2. убедиться что будет соблюден порядок доступа к памяти - исключены попытки одновременного доступа на запись в одну и ту же область памяти, убедиться что записанные данные будут доступны на чтение что мы завершим операции чтения до того как этот объект будет перезаписан/сброшен или изменит свой формат. Это называют memory dependency (зависимость памяти).


Если не будут соблюдаться эти условия то результат может быть непредсказуемым, так как Вулкан не осуществляет никаких дополнительных проверок на доступность операции.


Однако, в связи с тем, что в этот процесс у нас вовлечено несколько сущностей, то задача несколько усложняется. В частности:
  1. Синхронизация между CPU (хостом) и GPU
  2. Синхронизация между очередями на GPU
  3. Синхронизация внутри очереди на GPU
  4. Синхронизация доступа к разделяемым ресурсам


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


Все описанные задачи решаются при помощи следующих примитивов: Fences, Semaphores, Events и Barriers. Пройдемся вкратце по каждому из них.


Fences - самая грубая синхронизация, добавляется вместе с пакетом записанный команд через команду vkQueueSubmit и сигнализирует о том, что этот засабмиченный пакет задач был выполнен. Состояние этого Fence-объекта можно проверить и заресетить только со стороны хоста, для этого существуют команды  vkWaitForFences и vkResetFences соответственно. При этом, даже если был получен сигнал что задачи завершены, то это не гарантирует что были так же завершены и все операции работы с памятью.


Semaphores - позволяют синхронизировать работу между различными очередями, и между задачами, засабмиченными в одну очередь. Семафоры включаются в структуру  VkSubmitInfo  и сабмитятся в очередь через vkQueueSubmit. Сигнал семафорам отправляется по завершению выполнения всех задач в пакете.
Все засабмиченные пакеты задач, в этой или другой очереди, дойдя до определенной стадии выполнения, заданной маской в pWaitDstStageMask, будут ожидать пока не просигналит соответствующий семафор из pWaitSemaphores. После чего выполнение команд в очереди продолжится, пока очередь не дойдет до следующей стадии ожидающей сигнала от какого-то из семафоров.


Ожидающие семафоры устанавливаются на следующие стадии конвейера:
typedef enum VkPipelineStageFlagBits {
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT = 0x00000001,
VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT = 0x00000002,
VK_PIPELINE_STAGE_VERTEX_INPUT_BIT = 0x00000004,
VK_PIPELINE_STAGE_VERTEX_SHADER_BIT = 0x00000008,
VK_PIPELINE_STAGE_TESSELLATION_CONTROL_SHADER_BIT = 0x00000010,
VK_PIPELINE_STAGE_TESSELLATION_EVALUATION_SHADER_BIT = 0x00000020,
VK_PIPELINE_STAGE_GEOMETRY_SHADER_BIT = 0x00000040,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT = 0x00000080,
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT = 0x00000100,
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT = 0x00000200,
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT = 0x00000400,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT = 0x00000800,
VK_PIPELINE_STAGE_TRANSFER_BIT = 0x00001000,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT = 0x00002000,
VK_PIPELINE_STAGE_HOST_BIT = 0x00004000,
VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT = 0x00008000,
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT = 0x00010000,
} VkPipelineStageFlagBits;


Семафоры работают только на стороне GPU и их состояние не может быть получено на стороне CPU.


Events - события, создаваемые командой vkCreateEvent, это “инструмент тонкой настройки”, позволяющий добавлять в буфер команд команды установки (vkCmdSetEvent), сброса (vkCmdResetEvent) и ожидания события (vkCmdWaitEvents). При этом состояние этого события может быть прочитано, установлено или сброшено со стороны хоста командами vkSetEvent, vkGetEventStatus и vkResetEvent соответственно. Команды vkCmdSetEvent и vkCmdResetEvent влияют только на события в очереди, в которую они добавлены и не видны другим очередям.


Если с синхронизацией с хостом думаю все понятно, то на синхронизации на основе событий без участия хоста нужно остановиться немного подробнее. Проблема в том, что команды, записанные в очередь, могут выполняться в произвольном порядке, потому добавление в очередь команды vkCmdWaitEvents не гарантирует что событие будет ожидаться именно в этом месте, именно после этой команды и именно перед следующей командой. Это же касается и команд vkCmdSetEvent и vkCmdResetEvent. Так же как и в случае семафоров привязаться мы можем лишь к стадии конвейера. В случае команд vkCmdSetEvent и vkCmdResetEvent мы в stageMask указываем стадию при которой происходит установка или сброс соответствующего события. В случае  vkCmdWaitEvents все немного иначе. В первую очередь необходимо передать туда список всех событий (pEvents), которые мы будем ожидать, во-вторых нужно в srcStageMask указать все стадии, на которых будут срабатывать указанные в pEvents события (те stageMask, которые мы заполняли при вызове vkCmdSetEvent и vkCmdResetEvent), зачем это нужно в спецификации не указано, возможно внутренняя оптимизация и сортировка событий, в-третьих - нужно заполнить маску dstStageMask, в этой маске указываются стадии конвейера на которых будет происходить ожидание событий из dstStageMask. Если предполагается ожидание сигнала события со стороны хоста, то srcStageMask должно быть равно VK_PIPELINE_STAGE_HOST_BIT.


Как это все работает, как только наступила стадия, указанная в dstStageMask - выполнение команд в очереди приостанавливается и мы ожидаем пока не просигналит eventCount событий (поочередно или сразу), после чего выполняются барьеры памяти, указанные в pMemoryBarriers, pBufferMemoryBarriers и pImageMemoryBarriers. После того как барьеры отработают - выполнения стадии, указанной в dstStageMask продолжается.


Более подробно о том, как разруливаются ситуации с разными вариантами установки srcStageMask и dstStageMask можно почитать в спецификации или здесь же, в переводе.


Ну и четвертый тип синхронизации это барьеры(Barriers), они устанавливаются между двумя командами, работающими с памятью объектов - глобальной памятью, памятью буфера и памятью изображения. Причем на каждый тип объектов существует свой тип барьера, определяемого через структуры VkMemoryBarrier,  VkBufferMemoryBarrier и VkImageMemoryBarrier соответственно. Барьеры можно указать как реакцию на событие (vkCmdWaitEvents) или указать явно через vkCmdPipelineBarrier.
Так же как и для vkCmdWaitEvents, у vkCmdPipelineBarrier есть две маски, srcStageMask и dstStageMask, srcStageMask указывает какие стадии конвейера должны завершиться перед установкой барьеров, а в dstStageMask указывается какие стадии не должны начаться до того как отработают барьеры.
Задача барьера - исключить одновременный доступ двух команд к одной области памяти, если одна из команд (или обе) изменяют эту память (под изменение подразумевается как непосредственный доступ к памяти на запись, так и изменение структуры (формата) данных), при этом, если обе команды обращаются к области памяти только на чтение, то в установке барьера нет необходимости.
Тип доступа к памяти задается через комбинацию битов VkAccessFlagBits:

typedef enum VkAccessFlagBits {
VK_ACCESS_INDIRECT_COMMAND_READ_BIT = 0x00000001,
VK_ACCESS_INDEX_READ_BIT = 0x00000002,
VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT = 0x00000004,
VK_ACCESS_UNIFORM_READ_BIT = 0x00000008,
VK_ACCESS_INPUT_ATTACHMENT_READ_BIT = 0x00000010,
VK_ACCESS_SHADER_READ_BIT = 0x00000020,
VK_ACCESS_SHADER_WRITE_BIT = 0x00000040,
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT = 0x00000080,
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT = 0x00000100,
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT = 0x00000200,
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT = 0x00000400,
VK_ACCESS_TRANSFER_READ_BIT = 0x00000800,
VK_ACCESS_TRANSFER_WRITE_BIT = 0x00001000,
VK_ACCESS_HOST_READ_BIT = 0x00002000,
VK_ACCESS_HOST_WRITE_BIT = 0x00004000,
VK_ACCESS_MEMORY_READ_BIT = 0x00008000,
VK_ACCESS_MEMORY_WRITE_BIT = 0x00010000,
} VkAccessFlagBits;


Тонкости использования каждого типа барьера, случаи когда такая синхронизации не требуется, или когда она осуществляется автоматически, а так же возможные комбинации srcAccessMask и dstAccessMask вы можете узнать из спецификации или здесь же, в переведенном виде.

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

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

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

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