Цепочка зависимостей выполнения для переходов макета изображения (и правильное использование VkPipelineStageFlagBits и VkAccessFlagBits для этого случая) - PullRequest
0 голосов
/ 25 марта 2020

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

void perform_image_layout_transition(VkImage theImage, VkImageLayout oldLayout, VkImageLayout newLayout, ...)
{
    VkImageMemoryBarrier barrier = {};
    barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
    barrier.oldLayout = oldLayout;
    barrier.newLayout = newLayout;
    barrier.srcAccessMask = 0;
    barrier.dstAccessMask = 0;
    barrier.image = theImage;

    vkCmdPipelineBarrier(..., VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, ... , 1, &barrier);
}

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

// 1. Synchronize with whatever comes before
VkMemoryBarrier memBarrBefore = {};
memBarrBefore.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER;
memBarrBefore.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT; // Make previous writes available
memBarrBefore.dstAccessMask = 0; // No need to make memory visible; if it is available, that's fine.

vkCmdPipelineBarrier(..., 
    previousStageToSynchronizeWith,
    VK_PIPELINE_STAGE_TRANSFER_BIT,
    0,
    1, &memBarrBefore, 
    ...
);

// 2. Layout transition
perform_image_layout_transition(...);

// 3. Synchronize with whatever comes after
VkMemoryBarrier memBarrAfter = {};
memBarrAfter.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER;
memBarrAfter.srcAccessMask = 0; // Memory is already available. Hence, no need to specify an access mask.
memBarrAfter.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT; // Make memory visible to the subsequent command

vkCmdPipelineBarrier(..., 
    VK_PIPELINE_STAGE_TRANSFER_BIT,
    subsequentStageToSynchronizeWith,
    0,
    1, &memBarrAfter, 
    ...
);

Мой вопрос: это жизнеспособный подход для синхронизации стадии previousStageToSynchronizeWith с subsequentStageToSynchronizeWith, делающей всю память из первой доступной и видимой для всех кэшей последнего?

Я не уверен, что выполнение цепочка зависимостей над VK_PIPELINE_STAGE_TRANSFER_BIT - это правильный подход / выбор. Могу ли я также использовать другой этап конвейера для создания такой зависимости выполнения? Или VK_PIPELINE_STAGE_TRANSFER_BIT единственный возможный выбор? Или это даже неправильно, может быть?


Дополнительный вопрос, основанный на ответе krOoze:

Давайте предположим, что обновленная версия perform_layout_transition использует VK_PIPELINE_STAGE_BOTTOM_OF_PIPE в соответствии с предложением:

void perform_image_layout_transition(VkImage theImage, VkImageLayout oldLayout, VkImageLayout newLayout, ...)
{
    VkImageMemoryBarrier barrier = {};
    barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
    barrier.oldLayout = oldLayout;
    barrier.newLayout = newLayout;
    barrier.srcAccessMask = 0;
    barrier.dstAccessMask = 0;
    barrier.image = theImage;

    vkCmdPipelineBarrier(..., VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, ... , 1, &barrier);
}

Действительно ли это правильно синхронизируется с последующими TRANSFER операциями - т.е., например, с двумя последующими vkCmdCopy* операциями с переходом макета между ними? В исходной версии цепочка зависимостей выполнения от
before -> TRANSFER -> TRANSFER -> TRANSFER -> TRANSFER -> after очевидна.
Но в обновленной версии это будет
before -> TRANSFER -> BOTTOM -> BOTTOM -> TRANSFER -> after.

Действительно ли vkCmdPipelineBarrier(..., VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, ... , 1, &barrier); гарантирует, что - в примере двух последующих операций vkCmdCopy* - вторая vkCmdCopy* ждет, пока не будет выполнен переход макета?

Что я не могу осознать, в частности, это следующее (т.е. это моя личная ментальная модель, которая в какой-то момент должна быть неверной):

  1. Первая передача команда проходит через этапы: TOP -> TRANSFER -> BOTTOM.
  2. Между ними идет НЕТ команды (?) только переход макета, который не проходит через какие-либо этапы (?)
  3. Вторая команда передачи выполняется через этапы: TOP -> TRANSFER -> BOTTOM.

Теперь, если я использую обновленную версию, которая создает цепочку зависимостей выполнения между BOTTOM и BOTTOM, не может ли быть так, что переход макета изображения еще не завершился до второй команды передачи TRANSFER этап начинается? Синхронизация BOTTOM -> BOTTOM будет означать только, что переход макета выполняется после BOTTOM первой команды передачи и до BOTTOM второй команды передачи, или это так? Где я не прав?


Уточняющий вопрос:

В своем исходном вопросе (самый верхний) я рассматривал следующую ситуацию:

vkCmdCopy*(...);
vkCmdPipelineBarrier(..., TRANSFER, TRANSFER, /* memory barrier to make memory available, but no layout transition */);
perform_image_layout_transition(TRANSFER, TRANSFER, ...);
vkCmdPipelineBarrier(..., TRANSFER, TRANSFER, /* memory barrier to make memory visible, but no layout transition */);
vkCmdCopy*(...);

Затем было предложено использовать BOTTOM_OF_PIPE stage для перехода к макету изображения, который сводится к следующему:

vkCmdCopy*(...);
vkCmdPipelineBarrier(..., TRANSFER, TRANSFER, /* memory barrier to make memory available, but no layout transition */);
perform_image_layout_transition(BOTTOM_OF_PIPE, BOTTOM_OF_PIPE, ...);
vkCmdPipelineBarrier(..., TRANSFER, TRANSFER, /* memory barrier to make memory visible, but no layout transition */);
vkCmdCopy*(...);

И после отличного описания ментальной модели по этапам конвейера, Я все еще не вижу, чтобы две vkCmdCopy* команды были правильно синхронизированы с переходом макета между ними.

spe c говорит:

Цепочка зависимостей выполнения последовательность зависимостей выполнения, которые формируют отношение «до и после» между первой зависимостью A 'и конечной зависимостью B'. Для каждой последовательной пары зависимостей выполнения существует цепочка, если пересечение B в первой зависимости и As во второй зависимости не является пустым набором.

Не означает ли это, что vkCmdPipelineBarrier(..., TRANSFER, TRANSFER, ...) и perform_image_layout_transition(BOTTOM_OF_PIPE, BOTTOM_OF_PIPE, ...) не сможет создать цепочку зависимостей выполнения, поскольку пересечение их флагов этапа является пустым набором? Кроме того, первый vkCmdPipelineBarrier не будет образовывать цепочку зависимостей выполнения со вторым vkCmdPipelineBarrier, поскольку они не являются «последовательной парой»?!

Может быть, возможно / будет работать следующая версия:

vkCmdCopy*(...);
vkCmdPipelineBarrier(..., TRANSFER, BOTTOM_OF_PIPE, /* memory barrier to make memory available, but no layout transition */);
perform_image_layout_transition(BOTTOM_OF_PIPE, BOTTOM_OF_PIPE, ...);
vkCmdPipelineBarrier(..., BOTTOM_OF_PIPE, TRANSFER, /* memory barrier to make memory visible, but no layout transition */);
vkCmdCopy*(...);

Может быть, однако, эта версия все время имела в виду krOoze?

1 Ответ

1 голос
/ 25 марта 2020

Мне кажется, это должно сработать (плюс минус формализм c Выпуск KhronosGroup / Vulkan-Docs # 1193 ).

Но это лишает смысла использование Vulkan. У вас есть три барьера, где должен быть один (или меньше при дозировании). И вы используете более жестокие маски доступа. И вместо этого быть частью прохода рендеринга может быть лучше (практически бесплатно как часть load \ store op) на некоторых платформах Вы либо просите Vulkan драйвер быть очень умным (чего не должно быть), либо это ухудшит производительность. Это не очень хорошее начало для фреймворка и сводится в основном только к проверке флажка Vulkan, хотя такая наивная реализация, вероятно, работает хуже, чем OpenGL.

Я не уверен, что цепочка зависимостей выполнения превышает VK_PIPELINE_STAGE_TRANSFER_BIT - это правильный подход / выбор.

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

Хотя уровень Best Practices Жалуйтесь с предупреждением, если он видит, что вы переходите к непередаваемому макету изображения, в то время как сцена передается.

Я бы предпочел использовать BOTTOM_OF_PIPE. Хотя, если вы не можете изменить эти этапы в perform_image_layout_transition, у вас нет выбора.


Да, я думаю, что ваша ментальная модель конвейера неверна (и я думаю, что это распространенная ошибка) , Вы думаете о конвейере как о FSM (или блок-схеме). Но это не FSM (несмотря на то, что обманчиво имеет такое же графическое представление). Это буквально «конвейер».

В FSM вы начинаете с ТОПа, затем вы проходите go через все этапы, затем вы достигнете BOTTOM. И тогда ты был бы навсегда сделан. Но это , а не , что такое конвейер.

Конвейер работает по-другому. Все этапы конвейера просто есть; они всегда существуют, и все они всегда «исполняются» в любой момент времени. Они никогда не начинаются и никогда не заканчиваются. Думайте о конвейере как о настольной игре.

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

Также имейте в виду, что команды (колышки) могут быть в несколько этапов в данный момент времени. Как правило, на современном GPU vkCmdDraw запускает некоторые вершинные шейдеры, в какой-то момент он начинает вызывать фрагментные шейдеры (поэтому он находится на и VERTEX и FRAGMENT этапах), затем в какой-то момент у него заканчиваются вершины (так он выходит из этапа VERTEX и находится только на этапе FRAGMENT).

Что делает зависимость (Subpass Dependency или Pipeline Barrier) только введением правила игры, согласно которому один колышек не должен достигать определенного поля, если другой колышек имеет еще не достигло определенного другого поля на плате.

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

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

Per spe c, цепочки зависимостей, пока их маски стадий являются подмножеством. И когда их цепочка означает, что они действуют как единичная зависимость, которая имеет src первой зависимости и имеет dst второй зависимости. Таким образом, две зависимости, которые образуют цепочку, ведут себя как три зависимости; в качестве двух зависимостей в отдельности и как цепочечная зависимость.


vkCmdPipelineBarrier(..., TRANSFER, TRANSFER, ...);
perform_image_layout_transition(BOTTOM_OF_PIPE, BOTTOM_OF_PIPE, ...);

все равно будет образовывать цепочку зависимостей, потому что (spe c VkPipelineStageFlagBits ):

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

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

Пересечение TRANSFER | BOTTOM с BOTTOM равно BOTTOM.

И да, (TRANSFER -> BOTTOM) -> (BOTTOM -> TRANSFER) - это то, что я предложил. В принципе любой (TRANSFER -> X) -> (X -> TRANSFER) будет технически действительным. Ваш вариант использования немного надуманный, так что это немного предположение. Но если ваши барьеры стоят спиной к спине, я бы предпочел X = BOTTOM в качестве защитной меры. Водителю трудно его неправильно истолковать; это должно означать не-оп. И как бы плохо ни был ваш вариант использования, возможно, он делает его немного лучше (а может и нет). Хотя нужно учитывать, что два исходных барьера действуют как цепь, а также как исходный (так как три отдельных барьера). Если у вас есть другие вещи между барьерами, возможно, X = TRANSFER будет лучше. TRANSFER не взаимодействует ни с COMPUTE, ни с какими-либо графическими этапами, как показано здесь . Последнее, как я обычно предпочитаю, когда работает с WSI . В конце вам нужно попробовать и измерить.

...