понедельник, 14 марта 2016 г.

Vulkan API, Barriers, Execution And Memory Dependencies.

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

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

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


Render pass - представляет коллекцию аттачментов, subpass-ов, зависимостей между subpass-ами и описывают как аттачменты используются в процессе работы subpass-ов. То что помещается в буфер команд называется инстансом render pass-а.
Дискрипторы аттачментов описывают свойства аттачментов, включая их формат, количество сэмплов и как их содержимое интерпретируется в начале и конце каждого инстанса render pass-а.

Subpass представляет фазу рендеринга чтения/записи в аттачменты render pass-а. Команды рендеринга записываются в отдельный subpass инстанса render pass-а.

Subpass description - описывает набор аттачментов, используемых при выполнении subpass-а. Каждый из subpass-ов может читать из некоторых входных аттачментов, писать в некоторые из color attachments (цветовых вложений) или depth/stencil attachments (вложения глубины/трафарета), а так же разрешающие операции через resolve attachments. Дескриптор subpass-а так же может включать набор preserve attachments (законсервированных вложений), вложения, недоступные subpass-ам на чтение/запись, но чье содержимое должно быть сохранено через subpass.

P.S. О самой концепции проходов и под-проходов рендера можно почитать здесь.

Subpass dependencies - описывают ограничения упорядочения между парами subpass-ов. Если зависимость не указана, реализация Вулкана может менять порядок или перекрывать части выполнения subpass-ов (к примеру - определенные шейдерные стадии). Зависимости (dependencies) ограничивают степень перекрытия или изменения порядка выполнения subpass-ов и определяется через маски стадий конвейера и типы доступа к памяти. Каждая из зависимостей действует как зависимость выполнения и как зависимость памяти (execution и memory dependency). Зависимости необходимы когда два subpass-а работают с одним аттачментом с перекрывающимися диапазонами, подобными объектам VkDeviceMemory, и как минимум один из subpass-ов осуществляет запись в этот диапазон.

Subpass dependency chain - последовательность зависимостей subpass-а в render pass-е, где начальный subpass каждой из зависимостей (после первой) равен конечному subpass-у предыдущей зависимости.

Self-Dependency - Зависимость subpass-а от самого себя, то есть когда srcSubpass равен dstSubpass. Self-dependency не выполняется автоматически во время работы инстанса render pass-а, но их подмножество может быть выполнено через vkCmdPipelineBarrier во время работы subpass-а.

Subset (Self-Dependency) - подмножество из self-dependency это барьеры конвейера выполняющиеся в течении subpass-а self-dependency, и чьи маски стадий и доступа содержат подмножество битов, установленных в таких же масках в self-dependency.


Execution And Memory Dependencies

Команды синхронизации предоставляют явную зависимость порядка выполнения и доступа к памяти между двумя наборами команд действия, где второй набор команд зависит от первого набора команд. Возможно три сценария:
• команды после vkCmdWaitEvents, в той же очереди, использующие то же событие зависят от команд стоящих перед vkCmdSetEvent.
• команды в subpass-ах с большими номерами, или команды после инстанса renderpass-а, где есть зависимость между двумя subpass-ми, или между subpass-ом и VK_SUBPASS_EXTERNAL, зависят от команд subpass-ов с меньшими номерами и команд идущих перед инстансом renderpass-а.
• Команды в очереди конвейера зависят от команд в той же очереди, стоящих перед барьером.

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

Цепочка зависимостей выполнения, от набора исходных стадий конвейера А до набора конечных стадий конвеера В, это последовательность зависимостей выполнения отправленных в очередь в между первым набором команд и вторым набором команд, удовлетворяющих следующим условиям:
• Первая зависимость включает в srcStageMask А или флаг VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT или флаг VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, и
• Конечная зависимость включает в dstStageMask B или флаг  VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT или флаг  VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, и
• для каждой зависимости в последовательности (кроме первой) как минимум одно из следующих условий должно выполняться:
– srcStageMask текущей зависимости включает как минимум один бит C, представленный в dstStageMask предыдущей зависимостью, или
– srcStageMask текущей зависимости включает флаг VK_PIPELINE_STAGE_ALL_COMMANDS_BIT или флаг  VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, или,
– dstStageMask из предыдущей зависимости включает флаг  VK_PIPELINE_STAGE_ALL_COMMANDS_BIT или флаг VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, или
– srcStageMask текущей зависимости включает флаг VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT, и dstStageMask предыдущей зависимости включает как минимум одну графическую стадию конвейера, или
– dstStageMask из предыдущей зависимости включает флаг VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT, и srcStageMask текущей зависимости включает как минимум одну графическую стадию конвейера.

Пара последовательных зависимостей выполнения, в цепочке выполнения зависимостей, задает зависимость между стадиями А и В с помощью промежуточных стадий C, даже если задачи не используют стадии конвейера включенные в C.

Цепочка зависимостей выполнения гарантирует что задачи выполняемые стадиями конвейера А в первом сете команд были завершены прежде чем начнут выполняться задачи выполняемые стадиями конвейера В во втором сете команд.

Зависимость выполнения считается по-регионной, если ее параметр включает флаг VK_DEPENDENCY_BY_REGION_BIT. Такой барьер описывает по-регионную зависимость (x,y,layer). То есть, для каждого региона реализация Вулкана должна обеспечить чтоб исходные стадии для первого сета команд завершили выполнение до того, как команды из второго сета для этого же региона начнут выполнение. Так как вызовы фрагментного шейдера не задаются для какой-то конкретной группы, размер региона зависит от реализации Вулкана, он не известен приложению, и нужно предполагать что он может быть не больше одного пикселя. Если параметр dependencyFlags не включает бит VK_DEPENDENCY_BY_REGION_BIT, то это описывается глобальная зависимость, то есть для всех пикселей региона должны быть завершены все команды исходной стадии, до начала старта команд следующей стадии конвейера.

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

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

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

Когда барьер памяти изображения включает передачу структуры, то барьер сначала устанавливается на запись через srcStageMask и srcAccessMask, затем выполняется передача структуры, затем содержимое ресурсов изображения в новой структуре делается видимым для доступа к памяти через dstStageMask и dstAccessMask, так как если бы была зависимость выполнения между исходной маской и передачей, так же как и между передачей и конечной маской. Любые из произведенных ранее записей, которые были сделаны доступными, включаются при передаче структуры, остальные могут потеряется или исказить изображение.

Все зависимости должны включать как минимум один бит в каждой из srcStageMask и  dstStageMask.

Зависимости памяти используются для разрешения рисков при работе с данными, к примеру, чтобы гарантировать, что операции записи доступны для последующих операций чтения (проблема чтения после записи), или риск записи после записи. Риски записи после чтения и чтение после чтение для синхронизации требуют только зависимость выполнения.

Прежде чем приступить к дальнейшему переводу спецификации я решил вставить сюда еще перевод обзорной статьи по барьерам от AMD:

Barriers
Барьеры это новая концепция представленная разработчикам, которая ранее была скрыта внутри драйвера. По аналогии с синхронизацией на CPU - у вас есть несколько пишущих потоков, обновляющих буфер, вы добавляете синхронизацию, чтоб убедиться что все операции записи были завершены, и затем вы можете начать обработку этих данных несколькими читающими потоками. Однако, этим барьеры (vkCmdPipelineBarrier) на стороне GPU не ограничиваются, здесь у него в общем есть три функции:
  • Синхронизация, убеждаемся что все предыдущие задачи, от которых мы зависим, были завершены, прежде чем мы начнем новые задачи.
  • Видимость, убеждаемся что данные, закешированные отдельным юнитом, видимые другим юнитам, которые заходят их увидеть.
  • Переформатирование, убеждаемся что данные будут читаться юнитом в понятном ему формате.

Вы можете задаться вопросом, когда происходит переформатирование. Это часто происходит когда в игру вступает компрессия. К примеру, рендер таргет обычно находится в сжатом формате, чтоб снизить требуемую ширину полосы для его чтения и запсии. Некоторые модули GPU могут не поддерживать все форматы сжатия, потому может потребоваться распаковка данных. Обычно барьеры конвертируются в сброс кэшей (L1-кэш прикрепляется к отдельным юнитам, таким как буфер цвета, буфер глубины и/или более унифицированному L2-кэшу), ожидание, (к примеру, пока не завершится вычислительный шейдер прежде чем стартанет графический шейдер, который захочет начать читать результат) и/или распаковку.

Теперь мы знаем что делают барьеры, почему они так важны? Барьеры часто это причина ошибок как в Direct3D® 12 так и в Vulkan™. Большинство мерцаний, блочные артефакты, искажение данных и поломанная геометрия могут быть следствием неправильных барьеров. В первую очередь, убедитесь что барьеры на 100% правильные, не забывая следить за их ресурсами. То есть, если вы пишете в один слой массива текстуры, то этот слой должен быть переведен в правильное состояние, о чем часто забывают. Вы так же должны убедиться что все транзакции на месте, некоторые из них легко упустить из виду, к примеру - переход к отображению.

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

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

Fences
Так же как и барьеры Fences (vkCreateFence) используются для синхронизации CPU с GPU и между различными очередями на GPU. Fence - это очень тяжеловесный примитив синхронизации, так как требует от GPU как минимум сбросить все кэши, и, потенциально, выполнить некоторую дополнительную синхронизацию. Из-за этих расходов, fence нужно использовать с осторожностью, в частности, пытайтесь сгруппировать по-кадровые ресурсы и отслеживать их вместе через один fence вместо трекинга каждого ресурса по отдельности. Для инстансов, все буферы команд, использующиеся в одном кадре, должны быть защищены одним fence-ом, вместо того, чтоб добавлять по одному fence-объекту в каждый буфер команд.
Fence так же нужно использовать с осторожностью при по-кадровой синхронизации очередей вычислений, копирования и графических. В идеале, для получения наилучшей производительности, пытайтесь отправлять большие группы асинхронных вычислительных задач или задач копирования данных с одним fence-объектом в конце, который сообщит о том, что все задачи были завершены.

Подведем итоги:
  • Количество барьеров должно быть примерно таким же как и количество ресурсов, в которые производится запись.
  • Стоимость каждого fence-объекта примерно такая же как один вызов  ExecuteCommandList (стоимость CPU и GPU).
  • Не используйте “read-to-read” барьеры, в первую очередь переведите ресурсы в правильное состояние.

Pipeline Barriers

Барьер пайплана вставляется между двумя наборами команд в буфере команд через вызов:
void vkCmdPipelineBarrier(
VkCommandBuffer commandBuffer,
VkPipelineStageFlags srcStageMask,
VkPipelineStageFlags dstStageMask,
VkDependencyFlags dependencyFlags,
uint32_t memoryBarrierCount,
const VkMemoryBarrier * pMemoryBarriers,
uint32_t bufferMemoryBarrierCount,
const VkBufferMemoryBarrier * pBufferMemoryBarriers,
uint32_t imageMemoryBarrierCount,
const VkImageMemoryBarrier * pImageMemoryBarriers);
• commandBuffer - буфер команд, куда будет вставлен барьер.
• srcStageMask - битовая маска VkPipelineStageFlagBits задающая начальный набор стадий конвеера, не 0.
• dstStageMask - битовая маска, задающая набор конечных стадий конвеера, не 0.
• dependencyFlags - битовая маска VkDependencyFlagBits, если маска включает бит VK_DEPENDENCY_BY_REGION_BIT, то зависимость выполнения будет по-регионной.
• memoryBarrierCount - количество элементов массива pMemoryBarriers.
• pMemoryBarriers - указатель на массив структур VkMemoryBarrier.
• bufferMemoryBarrierCount - количество элементов массива pBufferMemoryBarriers.
• pBufferMemoryBarriers - указатель на массив структур VkBufferMemoryBarrier.
• imageMemoryBarrierCount - количество элементов массива pImageMemoryBarriers.
• pImageMemoryBarriers - указатель на массив структур VkImageMemoryBarrier.

• Если геометрический шейдер не активен, то srcStageMask и dstStageMask не должны содержать флаг VK_PIPELINE_STAGE_GEOMETRY_SHADER_BIT
• Если тесселяция не активна. то srcStageMask и dstStageMask не должны содержать флагов VK_PIPELINE_STAGE_TESSELLATION_CONTROL_SHADER_BIT и VK_PIPELINE_STAGE_TESSELLATION_EVALUATION_SHADER_BIT
• Если vkCmdPipelineBarrier вызван внутри инстанса рендер пасса, тогда рендер пасс должен объявить как минимум одну зависимость из текущего сабпасса на самого себя.

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

Subpass Self-dependency

Если vkCmdPipelineBarrier вызывается внутри инстанса рендер-пасса, применяются следующие ограничения. Чтоб для данного сабпасса был допустим барьер, рендер пасс должен объявить зависимость себя от этого садбасса. Тоесть, в списках зависимостей сабпасса для рендер пасса должен существовать объект VkSubpassDependency, в котором srcSubpass и dstSubpass равны индексу данного сабпасса. Можно объявлять более одной зависимости для каждого сабпасса.
Self-Dependency должна включать только биты графических стайдий конвеера.
Self-Dependency не должна иметь никаких ранних стадий конвеера, зависящих от каких либо более поздних стадий конвеера. Если более точно, то это означает что любая из последних стадий конвеера из srcStageMask не должна быть позже любой из первых стадий конвеера в dstStageMask (последняя из исходных стадий может быть равной первой из конечных стадий). Если обе маски, исходной и конечной стадий, включают стадии фрэймбуфера, то dependencyFlags должен включать флаг VK_DEPENDENCY_BY_REGION_BIT.

Команда vkCmdPipelineBarrier внутри инстанса рендер пасса должна быть одной из собственных зависимостей сабпасса, который ее использует, предполагая что маска стадий и маска доступов должны включать биты из соответствующей маски собственной зависимости. Если Self-Dependency включает флаг VK_DEPENDENCY_BY_REGION_BIT, то должен быть и барьер пайплана. Барьер конвеера внутри прохода ренедра может иметь тип только VkMemoryBarrier или  VkImageMemoryBarrier. Если используется VkImageMemoryBarrier, тогда изображение и все связанные ресурсы, указанные в барьере, должны быть из одного из image views, используемых фрэймбуфером в текущем сабпассе. В дополнение, oldLayout должен быть равен newLayout и оба srcQueueFamilyIndex и dstQueueFamilyIndex должны быть VK_QUEUE_FAMILY_IGNORED.

Pipeline Stage Flags

Команды, работающие с событиями, vkCmdPipelineBarrier и VkSubpassDependency, зависят от возможности указать где в логическом конвейере события могут сигнализировать или от исходной и конечно зависимостей выполнения.
Эти стадии конвейера определяются следующими битами:
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;

Смысл каждого из них:
• VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT: Стадия конвейера где команды первоначально получены очередью.
• VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT: Стадия конвейера где используются структуры данных Draw/DispatchIndirect.
• VK_PIPELINE_STAGE_VERTEX_INPUT_BIT: Стадия конвейера где используются вершинный и индексный буферы.
• VK_PIPELINE_STAGE_VERTEX_SHADER_BIT: Стадия вершинного шейдера.
• VK_PIPELINE_STAGE_TESSELLATION_CONTROL_SHADER_BIT: Стадия управляющего шейдера тесселяции.
• VK_PIPELINE_STAGE_TESSELLATION_EVALUATION_SHADER_BIT: Стадия вычислительного шейдера тесселяции.
• VK_PIPELINE_STAGE_GEOMETRY_SHADER_BIT: Стадия геометрического шейдера.
• VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT: Стадия фрагментного шейдера.
• VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT: Стадия ранней проверки на тест глубины и трафарета, до обработки фрагмента.
• VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT: Стадия поздней проверки на тест глубины и трафарета, после выполнения фрагментного шейдера.
• VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT: Стадия конвейера после завершения блендинга, где конвейер выдает конечный цвет. Эта стадия так же включает разрешающие операции(resolve operations) выполняющиеся в конце subpass-а, но это еще не означает что значения были записаны в память.
• VK_PIPELINE_STAGE_TRANSFER_BIT: Выполнение команды копирования. Это включает в себя все результаты всех операций со всех команд передачи данных -  vkCmdCopyBuffer, vkCmdCopyImage, vkCmdBlitImage, vkCmdCopyBufferToImage, vkCmdCopyImageToBuffer, vkCmdUpdateBuffer, vkCmdFillBuffer, vkCmdClearColorImage, vkCmdClearDepthStencilImage, vkCmdResolveImage, and vkCmdCopyQueryPoolResults.
• VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT: Выполнение вычислительного шейдера.
• VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT: Финальная стадия конвейера, где все команды завершают выполнение.
• VK_PIPELINE_STAGE_HOST_BIT: Псевдо-стадия, указывающая что хост выполняет чтение/запись из памяти устройства.
• VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT: Выполнение всех графических стадий конвейера.
• VK_PIPELINE_STAGE_ALL_COMMANDS_BIT: Выполнение всех стадий поддерживаемых очередью.

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

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